Cython三分鐘入門
我最喜歡的是 Python ,它的代碼優雅而實用,可惜純粹從速度上來看它比大多數語言都要慢。大多數人也認為的速度和易于使用是兩極對立的 —— 編寫 C 代碼的確非常痛苦。而 Cython 試圖消除這種兩重性,并讓你同時擁有 Python 的語法和 C 數據類型和函數 —— 它們兩個都是世界上最好的。請記住,我絕不是我在這方面的專家,這是我的第一次 Cython 真實體驗的筆記:
編輯:根據一些我收到的反饋,大家似乎有點混淆 ——Cython 是用來生成 C 擴展到而不是獨立的程序的。所有的加速都是針對一個已經存在的 Python 應用的一個函數進行的。沒有使用 C 或 Lisp 重寫整個應用程序,也沒有手寫 C 擴展 。只是用一個簡單的方法來整合 C 的速度和 C 數據類型到 Python 函數中去。
現在可以說,我們能使下文的 great_circle 函數更快。所謂 great_circle 是計算沿地球表面兩點之間的距離的問題:
importmath
defgreat_circle(lon1,lat1,lon2,lat2):
radius = 3956 #miles
x = math.pi/180.0
a = (90.0-lat1)*(x)
b = (90.0-lat2)*(x)
theta = (lon2-lon1)*(x)
c = math.acos((math.cos(a)*math.cos(b)) +
(math.sin(a)*math.sin(b)*math.cos(theta)))
return radius*c
讓我們調用它 50 萬次并測定它的時間 :
importtimeit
lon1, lat1, lon2, lat2 = -72.345, 34.323, -61.823, 54.826
num = 500000
t = timeit.Timer("p1.great_circle(%f,%f,%f,%f)" % (lon1,lat1,lon2,lat2),
"import p1")
print "Pure python function", t.timeit(num), "sec"
約 2.2 秒 。它太慢了!
讓我們試著快速地用 Cython 改寫它,然后看看是否有差別:
importmath
defgreat_circle(float lon1,float lat1,float lon2,float lat2):
cdeffloat radius = 3956.0
cdeffloat pi = 3.14159265
cdeffloat x = pi/180.0
cdeffloat a,b,theta,c
a = (90.0-lat1)*(x)
b = (90.0-lat2)*(x)
theta = (lon2-lon1)*(x)
c = math.acos((math.cos(a)*math.cos(b)) + (math.sin(a)*math.sin(b)*math.cos(theta)))
return radius*c
請注意,我們仍然 importmath——cython 讓您在一定程度上混搭 Python 和 C 數據類型在。轉換是自動的,但并非沒有代價。在這個例子中我們所做的就是定義一個 Python 函數,聲明它的輸入參數是浮點數類型,并為所有變量聲明類型為 C 浮點數據類型。計算部分它仍然使用了 Python 的 math 模塊。
現在我們需要將其轉換為 C 代碼再編譯為 Python 擴展。完成這一部的最好的辦法是編寫一個名為 setup.py 發布腳本。但是,現在我們用手工方式 ,以了解其中的巫術:
<prename="code" class="plain"># this will create a c1.c file - the C source code to build a python extension
cythonc1.pyx
# Compile the object file
gcc -c -fPIC -I/usr/include/python2.5/ c1.c
# Link it into a shared library
gcc -sharedc1.o -o c1.so</pre>
現在你應該有一個 c1.so (或 .dll )文件,它可以被 Python import 。現在運行一下:
t = timeit.Timer("c1.great_circle(%f,%f,%f,%f)" % (lon1,lat1,lon2,lat2),
"import c1")
print "Cython function (still using python math)", t.timeit(num), "sec"
約 1.8 秒 。并沒有我們一開始期望的那種大大的性能提升。使用 python 的 math 模塊應該是瓶頸。現在讓我們使用 C 標準庫替代之:
cdefexternfrom "math.h":
float cosf(float theta)
float sinf(float theta)
float acosf(float theta)
defgreat_circle(float lon1,float lat1,float lon2,float lat2):
cdeffloat radius = 3956.0
cdeffloat pi = 3.14159265
cdeffloat x = pi/180.0
cdeffloat a,b,theta,c
a = (90.0-lat1)*(x)
b = (90.0-lat2)*(x)
theta = (lon2-lon1)*(x)
c = acosf((cosf(a)*cosf(b)) + (sinf(a)*sinf(b)*cosf(theta)))
return radius*c
與 import math 相應,我們使用 cdef extern 的方式使用從指定頭文件聲明函數(在此就是使用 C 標準庫的 math.h )。我們替代了代價高昂的的 Python 函數,然后建立新的共享庫,并重新測試:
t = timeit.Timer("c2.great_circle(%f,%f,%f,%f)" % (lon1,lat1,lon2,lat2),
"import c2")
print "Cython function (using trig function from math.h)", t.timeit(num), "sec"
現在有點喜歡它了吧? 0.4 秒 – 比純 Python 函數有 5 倍的速度增長。我們還有什么方法可以再提高速度? c2.great_circle ()仍是一個 Python 函數調用,這意味著它產生 Python 的 API 的開銷(構建參數元組等),如果我們可以寫一個純粹的 C 函數的話,我們也許能夠加快速度。
cdefexternfrom "math.h":
float cosf(float theta)
float sinf(float theta)
float acosf(float theta)
cdeffloat _great_circle(float lon1,float lat1,float lon2,float lat2):
cdeffloat radius = 3956.0
cdeffloat pi = 3.14159265
cdeffloat x = pi/180.0
cdeffloat a,b,theta,c
a = (90.0-lat1)*(x)
b = (90.0-lat2)*(x)
theta = (lon2-lon1)*(x)
c = acosf((cosf(a)*cosf(b)) + (sinf(a)*sinf(b)*cosf(theta)))
return radius*c
def great_circle(float lon1,float lat1,float lon2,float lat2,int num):
cdefint i
cdeffloat x
for i from 0 < = i < num:
x = _great_circle(lon1,lat1,lon2,lat2)
return x
請注意,我們仍然有一個Python函數( def ),它接受一個額外的參數 num。這個函數里的循環使用for i from 0 < = i< num: ,而不是更Pythonic,但慢得多的for i in range(num):。真正的計算工作是在C函數(cdef)中進行的,它返回float類型。這個版本只要0.2秒——比原先的Python函數速度提高10倍。
為了證明我們所做的已經足夠優化,可以用純C寫一個小應用,然后測定時間:
#include <math .h>
#include <stdio .h>
#define NUM 500000
float great_circle(float lon1, float lat1, float lon2, float lat2){
float radius = 3956.0;
float pi = 3.14159265;
float x = pi/180.0;
float a,b,theta,c;
a = (90.0-lat1)*(x);
b = (90.0-lat2)*(x);
theta = (lon2-lon1)*(x);
c = acos((cos(a)*cos(b)) + (sin(a)*sin(b)*cos(theta)));
return radius*c;
}
int main() {
int i;
float x;
for (i=0; i < = NUM; i++)
x = great_circle(-72.345, 34.323, -61.823, 54.826);
printf("%f", x);
}
用 gcc -lm -octest ctest.c 編譯它,測試用 time./ctest … 大約 0.2 秒 。這使我有信心,我 Cython 擴展相對于我的 C 代碼也極有效率(這并不是說我的 C 編程能力很弱)。
能夠用 cython 優化多少性能通常取決于有多少循環,數字運算和 Python 函數調用,這些都會讓程序變慢。已經有一些人報告說在某些案例上 100 至 1000 倍的速度提升。至于其他的任務,可能不會那么有用。在瘋狂地用 Cython 重寫 Python 代碼之前,記住這一點:
“ 我們應該忘記小的效率,過早的優化是一切罪惡的根源,有 97% 的案例如此。 “——DonaldKnuth
換句話說,先用 Python 編寫程序,然后看它是否能夠滿足需要。大多數情況下,它的性能已經足夠好了 …… 但有時候真的覺得慢了,那就使用分析器找到瓶頸函數,然后用 cython 重寫,很快就能夠得到更高的性能。
外部鏈接
WorldMill ( http://trac.gispython.org/projects/PCL/wiki/WorldMill ) —— 由 Sean Gillies 用 Cython 編寫的一個快速的,提供簡潔的 python 接口的模塊,封裝了用以處理矢量地理空間數據的 libgdal 庫。
編寫更快的 Pyrex 代碼( http://www.sagemath.org:9001/WritingFastPyrexCode )—— Pyrex ,是 Cython 的前身,它們有類似的目標和語法。
來自:http://python.jobbole.com/87368/