使用 ctypes 将 Python 运行速度提升 30 倍
簡介
當 Python 面臨運算密集型任務時,其速度總是顯得力不從心。要提升 Python 代碼運行速度有多種方法,如 ctypes、cython、CFFI 等,本篇文章主要從 ctypes 方面介紹如何提升 Python 的運行速度。
ctypes 是 Python 的內置庫,利用 ctypes 可以調用 C/C++ 編譯成的 so 或 dll 文件 (so 存在 linux/MacOS 中,dll 存在于 windows),簡單而言,就是將計算壓力較大的邏輯利用 C/C++ 來實現,然后編譯成 so 或 dll 文件,再利用 ctypes 加載進 Python,從而將計算壓力大、耗時較長的邏輯交于 C/C++ 去執行。如 Numpy、Pandas 這些庫其底層其實都是 C/C++ 來實現的。
下面代碼的運行環境為:MacOS 、 Python3.7.3
純 Python 實現
為了對比出使用 ctypes 后程序運行速度的變化,先使用純 Python 代碼實現一段邏輯,然后再利用 C 語言去實現相同的邏輯。
這里為了模仿運算密集任務,實現一段邏輯用于計算一個集合中點與點之間的距離以及實現一個操作字符串的邏輯,具體代碼如下:
''' 遇到問題沒人解答?小編創建了一個Python學習交流QQ群:857662006 尋找有志同道合的小伙伴, 互幫互助,群里還有不錯的視頻學習教程和PDF電子書! ''' import random import time# 點 class Point():def __init__(self, x, y):self.x = xself.y = yclass Test():def __init__(self, string, nb):self.string = stringself.points = []# 初始化點集合for i in range(nb):self.points.append(Point(random.random(), random.random()))self.distances = []# 增量字符串def increment_string(self, n):tmp = ""# 每個字符做一次偏移for c in self.string:tmp += chr(ord(c) + n)self.string = tmp# 這個函數計算列表中每個點之間的距離def distance_between_points(self):for i, a in enumerate(self.points):for b in self.points:# 距離公式self.distances.append(((b.x - a.x) ** 2 + (b.y - b.x) ** 2) ** 0.5)if __name__ == '__main__':start_time = time.time()test = Test("A nice sentence to test.", 10000)test.increment_string(-5) # 偏移字符串中的每個字符test.distance_between_points() # 計算集合中點與點之間的距離print('pure python run time:%s'%str(time.time()-start_time))上述代碼中,定義了 Point 類型,其中有兩個屬性,分別是 x 與 y,用于表示點在坐標系中的位置,然后定義了 Test 類,其中的 increment_string () 方法用于操作字符串,主要邏輯就是循環處理字符串中的每個字符,首先通過 ord () 方法將字符轉為 unicode 數值,然后加上對應的偏移 n,接著在通過 chr () 方法將數值轉換會對應的字符。
此外還實現了 distance_between_points () 方法,該方法的主要邏輯就是利用雙層 for 循環,計算集合中每個點與其他點的距離。使用時,創建了 10000 個點進行程序運行時長的測試。
多次執行這份代碼,其運行時間大約在 39.4 左右
python 1.py pure python run time:39.431304931640625使用 ctypes 提速度代碼
要使用 ctypes,首先就要將耗時部分的邏輯通過 C 語言實現,并將其編譯成 so 或 dll 文件,因為我使用的是 MacOS,所以這里會將其編譯成 so 文件,先來看一下上述邏輯通過 C 語言實現的具體代碼,如下:
''' 遇到問題沒人解答?小編創建了一個Python學習交流QQ群:857662006 尋找有志同道合的小伙伴, 互幫互助,群里還有不錯的視頻學習教程和PDF電子書! ''' #include <stdlib.h> #include <math.h>#點結構 typedef struct s_point {double x;double y; } t_point;typedef struct s_test {char *sentence; // 句子int nb_points; t_point *points; // 點double *distances; // 兩點距離,指針 } t_test;#增量字符串 char *increment_string(char *str, int n) {for (int i = 0; str[i]; i++)// 每個字符做一次偏移str[i] = str[i] + n;return (str); }#隨機生成點集合 void generate_points(t_test *test, int nb) {#calloc () 函數用來動態地分配內存空間并初始化為 0#其實就是初始化變量,為其分配內存空間t_point *points = calloc(nb + 1, sizeof(t_point));for (int i = 0; i < nb; i++){points[i].x = rand();points[i].y = rand();}# 將結構地址賦值給指針test->points = points;test->nb_points = nb; }#計算集合中點的距離 void distance_between_points(t_test *test) {int nb = test->nb_points; # 創建變量空間double *distances = calloc(nb * nb + 1, sizeof(double));for (int i = 0; i < nb; i++)for (int j = 0; j < nb; j++)#sqrt 計算平方根distances[i * nb + j] = sqrt((test->points[j].x - test->points[i].x) * (test->points[j].x - test->points[i].x) + (test->points[j].y - test->points[i].y) * (test->points[j].y - test->points[i].y));test->distances = distances; }其中具體的邏輯不再解釋,可以看注釋理解其中的細節,通過 C 語言實現后,接著就可以通過 gcc 來編譯 C 語言源文件,將其編譯成 so 文件,命令如下:
#生成 .o 文件 gcc -c fastc.c #利用 .o 文件生成so文件 gcc -shared -fPIC -o fastc.so fastc.o獲得了 fastc.so 文件后,接著就可以利用 ctypes 將其調用并直接使用其中的方法了,需要注意的是「Windows 系統體系與 Linux/MacOS 不同,ctypes 使用方式會有差異」,至于 ctypes 的具體用法,后面會通過單獨的文章進行討論。
ctypes 使用 fastc.so 的代碼如下:
''' 遇到問題沒人解答?小編創建了一個Python學習交流QQ群:857662006 尋找有志同道合的小伙伴, 互幫互助,群里還有不錯的視頻學習教程和PDF電子書! ''' import ctypes from ctypes import * from ctypes.util import find_library import time# 定義結構,繼承自ctypes.Structure,與C語言中定義的結構對應 class Point(ctypes.Structure):_fields_ = [('x', ctypes.c_double), ('y', ctypes.c_double)]class Test(ctypes.Structure):_fields_ = [('sentence', ctypes.c_char_p),('nb_points', ctypes.c_int),('points', ctypes.POINTER(Point)),('distances', ctypes.POINTER(c_double)),]# Lib C functions _libc = ctypes.CDLL(find_library('c')) _libc.free.argtypes = [ctypes.c_void_p] _libc.free.restype = ctypes.c_void_p# Lib shared functions _libblog = ctypes.CDLL("./fastc.so") _libblog.increment_string.argtypes = [ctypes.c_char_p, ctypes.c_int] _libblog.increment_string.restype = ctypes.c_char_p _libblog.generate_points.argtypes = [ctypes.POINTER(Test), ctypes.c_int] _libblog.distance_between_points.argtypes = [ctypes.POINTER(Test)]if __name__ == '__main__':start_time = time.time()# 創建test = {}test['sentence'] = "A nice sentence to test.".encode('utf-8')test['nb_points'] = 0test['points'] = Nonetest['distances'] = Nonec_test = Test(**test)ptr_test = ctypes.pointer(c_test)# 調用so文件中的c語言方法_libblog.generate_points(ptr_test, 10000)ptr_test.contents.sentence = _libblog.increment_string(ptr_test.contents.sentence, -5)_libblog.distance_between_points(ptr_test)_libc.free(ptr_test.contents.points)_libc.free(ptr_test.contents.distances)print('ctypes run time: %s'%str(time.time() - start_time))多次執行這份代碼,其運行時間大約在 1.2 左右
python 2.py ctypes run time: 1.2614238262176514相比于純 Python 實現的代碼快了 30 倍有余
結尾
本節簡單的討論了如何利用 ctypes 與 C/C++ 來提升 Python 運行速度,有人可能會提及使用 asyncio 異步的方式來提升 Python 運行速度,但這種方式只能提高 Python 在 IO 密集型任務中的運行速度,對于運算密集型的任務效果并不理想,最后歡迎學習 HackPython 的教學課程并感覺您的閱讀與支持。
總結
以上是生活随笔為你收集整理的使用 ctypes 将 Python 运行速度提升 30 倍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2019 Python100道 面试 题
- 下一篇: Python中有几种办法交换两个变量的值