Cython! Python和C兩個世界的交叉點
最近一週都沒有發部落格,因為發現一個好玩的東西---Cython!這週一直在研究這個。 雖然瞭解C和Python之後學Cython,語法上很簡單,但是為了探究它為何能快起來, 還是翻了蠻多的程式碼並且做了測試的。
開始
Cython的資料不多,主要有三個(中文的就不用看了,包括這篇部落格,我也不會講 詳細的Cython的語法等,只是大概感慨一下):
- ofollow,noindex" target="_blank">官方文件
- 官方wiki
- Cython: A Guide for Python Programmers
它的副檔名有三種,.pyx
,.pxd
,.pxi
(雖然UNIX下副檔名無意義,但是
對人來說還是有意義的):
-
pyx
主要是implementation file,實現寫在這裡面,相當於c裡面的.c
檔案。 -
pxd
宣告檔案,d代表declaration,相當於c裡面的標頭檔案的作用。 -
pxi
include files,主要是用來包含其他檔案,但是我還沒用過。
我們先來看一段程式碼和效能比較,我選擇的效能比較的程式碼很簡單,就是遞迴計算斐波那契 數列第36位,然後我們來看時間。首先看純c版本的程式碼:
#include <time.h> #include <stdio.h> int fib(int n) { if (n == 0 || n == 1) return n; return fib(n - 1) + fib(n - 2); } int main() { clock_t begin = clock(); fib(36); clock_t end = clock(); double time_spent = (double)(end - begin) / CLOCKS_PER_SEC; printf("spent time: %.2fms\n", time_spent * 1000); }
執行時間:
root@arch fib: cc fib.c && ./a.out spent time: 136.85ms root@arch fib: cc fib.c && ./a.out spent time: 135.86ms root@arch fib: cc fib.c && ./a.out spent time: 137.15ms root@arch fib: cc fib.c && ./a.out spent time: 135.95ms
然後我們看純Python版本:
In [1]: def fib(n): ...:if n in (0, 1): ...:return n ...:return fib(n - 1) + fib(n - 2) ...: In [2]: %timeit -n 3 fib(36) 3 loops, best of 3: 7.23 s per loop
(其實我有測過Java的效能,竟然比C還快,有JIT也不能這樣啊!)
接下來看看Cython和Cython包裝第一個純c版本的程式碼和執行時間:
cdef extern from "fib.c": cdef int fib(int n) cpdef int cfib(int n): return fib(n) cpdef int cyfib(int n): if n == 0 or n == 1: return n return cyfib(n - 1) + cyfib(n - 2) cpdef int pure_cython(int n): if n in (0, 1): return n return pure_cython(n - 1) + pure_cython(n - 2)
執行時間:
In [1]: from cyfib import cfib, cyfib, pure_cython In [2]: %timeit -n 3 cfib(36) 3 loops, best of 3: 85.9 ms per loop In [3]: %timeit -n 3 cyfib(36) 3 loops, best of 3: 69.9 ms per loop In [4]: %timeit -n 3 pure_cython(36) 3 loops, best of 3: 87.3 ms per loop
和純python版本是不是百倍的速度之差 :doge:
Cython為何能提速?
Cython的速度來源於何處?我們看到了上面的cython程式碼,都有標註型別。在Python中
所有的東西都是一個object,在其實現裡,就是所有的東西都是一個PyObject
,然後
裡面都是指標指來指去。每個物件想要確定其型別,都至少要通過對指標進行一次解引用,
看一下PyObject的定義:
typedef struct _object { _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; struct _typeobject *ob_type; } PyObject;
其中的ob_type
就是其型別。再例如屬性查詢,完整的C程式碼看這裡
我簡化了一下:
/* Generic GetAttr functions - put these in your tp_[gs]etattro slot */ PyObject * _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict) { // 初始化變數 PyTypeObject *tp = Py_TYPE(obj); PyObject *descr = NULL; PyObject *res = NULL; descrgetfunc f = NULL; Py_ssize_t dictoffset; PyObject **dictptr; // 先從MRO中找出描述符 descr = _PyType_Lookup(tp, name); if (descr != NULL) { // 如果描述符不為空 f = descr->ob_type->tp_descr_get; if (f != NULL && PyDescr_IsData(descr)) { // 如果是data描述符, 使用 __get__ return f(descr, obj, (PyObject *)obj->ob_type); } } if (dict != NULL) { // 找物件的 __dict__ return PyDict_GetItem(dict, name); } if (f != NULL) {// 不是data描述符,使用 __get__ return f(descr, obj, (PyObject *)Py_TYPE(obj)); } if (descr != NULL) { return descr; } raise AttributeError(); }
這都是要經過很多步驟的,而對於C這樣的靜態語言來說,在編譯的時候就確定了 型別,如果對於struct這樣的結構體,進行屬性查詢,其實就是計算出某個屬性 相對於struct起始位置的記憶體大小偏移量,然後直接跑過去訪問就行。
還有一點消耗,在於Python VM處理時的切換。不信我們來做個測試,寫一個fib.py 然後用cython把該檔案編譯成動態連結庫,然後進行測速:
def fib(n): if n in (0, 1): return n return fib(n - 1) + fib(n - 2)
為了測試方便,把原檔案重新命名為pyfib.py
(懶的寫setup.py
,其實可以通過寫
Extension來指定編譯成啥名兒的)。
In [1]: import fib, pyfib In [2]: %timeit -n 3 fib.fib(36) 3 loops, best of 3: 2.3 s per loop In [3]: %timeit -n 3 pyfib.fib(36) 3 loops, best of 3: 7.34 s per loop
可以開啟cython生成的fib.c
來看看,有好幾千行,但是定位到相關程式碼,首先
就可以看到函式宣告:
static PyObject *__pyx_pf_3fib_fib(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_n); ... __pyx_t_3 = __Pyx_PyInt_EqObjC(__pyx_t_1, __pyx_int_0, 0, 0);
可以看出Cython對於生成的程式碼,進行了優化動作,例如先假設n是整型。
分析,除錯
cythonize -a cygdb
完結
一個型別系統對於語言的優化來說非常的重要,我一直感慨要是有一門語言能和 Python一樣寫起來爽,但是速度又能和C一樣快就好了!我想我找到了!
不過Cython仍然在發展中,很多地方都還可以改進,例如生成更可讀的c程式碼等。
Cython Rocks!