神技巧!將 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 語言去實現相同的邏輯。

這裡為了模仿運算密集任務,實現一段邏輯用於計算一個集合中點與點之間的距離以及實現一個操作字串的邏輯,具體程式碼如下:

import randomimport time# 點class Point(): def __init__(self, x, y): self。x = x self。y = yclass Test(): def __init__(self, string, nb): self。string = string self。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。pypure python run time:39。431304931640625

使用 ctypes 提速度程式碼

要使用 ctypes,首先就要將耗時部分的邏輯透過 C 語言實現,並將其編譯成 so 或 dll 檔案,因為我使用的是 MacOS,所以這裡會將其編譯成 so 檔案,先來看一下上述邏輯透過 C 語言實現的具體程式碼,如下:

#include #include // 點結構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 的程式碼如下:

import ctypesfrom ctypes import *from ctypes。util import find_libraryimport 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’] = 0 test[‘points’] = None test[‘distances’] = None c_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。pyctypes run time: 1。2614238262176514

相比於純 Python 實現的程式碼快了 30 倍有餘

結尾

本節簡單的討論瞭如何利用 ctypes 與 C/C++ 來提升 Python 執行速度,有人可能會提及使用 asyncio 非同步的方式來提升 Python 執行速度,但這種方式只能提高 Python 在 IO 密集型任務中的執行速度,對於運算密集型的任務效果並不理想。