前言
Python語言易用,開發(fā)效率高筹煮,適用范圍廣遮精,這些優(yōu)點(diǎn)是我們經(jīng)常提起的,幾乎做到了家喻戶曉吧败潦。但Python語言的性能也一直是大多數(shù)使用Python和沒使用過Python的人一直詬病的本冲。為什么沒有使用過Python的人也詬病Python的性能呢,這就涉及到更深入的話題了变屁,本篇不做深入眼俊。和我這樣的能力不足的程序員不同是的是,一直有一些聰明人在享受這Python的便利的同時(shí)也沒有放棄從各個(gè)方向優(yōu)化Python的性能粟关。其中Cython就是一個(gè)常用的方案疮胖,Cython可以做到像C一樣的高性能环戈,同時(shí)兼顧Python的簡單易用。Cython通過一個(gè)Python/C API來完成兩個(gè)語言之間的交流澎灸,以提供在計(jì)算密集型任務(wù)上對(duì)Python的良好性能支持院塞,這里要注意的是只在計(jì)算密集型任務(wù)上適應(yīng)這樣的解決方案,大部分應(yīng)用不需要這么做性昭,反而會(huì)適得其反拦止。
原生Python
上節(jié)說到只有那些計(jì)算密集型任務(wù)才考慮做性能優(yōu)化,沒有充分的理由不要過早做性能優(yōu)化糜颠,要知道過早的優(yōu)化是萬惡之源汹族。所以我們的例子也是舉一個(gè)計(jì)算密集型示例。計(jì)算一定范圍內(nèi)那些數(shù)字是素?cái)?shù)是一個(gè)典型的計(jì)算密集型任務(wù)其兴。我們這次就用這個(gè)作為示例做演示顶瞒。首先是Python語言實(shí)現(xiàn)部分:
from math import sqrt
def primes(n):
results = [1,]
for i in range(2, n):
for j in range(2, int(sqrt(i))):
if i % j == 0:
break
else:
results.append(i)
return results
def main():
primes(3000000)
if __name__ == '__main__':
main()
我對(duì)這段代碼做個(gè)簡單的解釋,代碼分為三個(gè)部分:第一段是功能的主題元旬,負(fù)責(zé)完成主要功能榴徐;第二段是主函數(shù)定義,就只是一個(gè)對(duì)primes的調(diào)用而已匀归;第三段是實(shí)現(xiàn)運(yùn)行當(dāng)前文件的時(shí)候自動(dòng)調(diào)用main函數(shù)坑资。所有的功能都在primes函數(shù)里面,這個(gè)函數(shù)在一個(gè)數(shù)字n的范圍內(nèi)穆端,求出所有這個(gè)范圍內(nèi)的素?cái)?shù)的列表袱贮。1是公認(rèn)的素?cái)?shù),所以我們直接加入到列表中体啰。以下數(shù)字從2開始直到n字柠,我們給所有的數(shù)字i挨個(gè)除以2直到它的平方根。如果都不能整除就跳出循環(huán)并把結(jié)果加入到素?cái)?shù)列表中狡赐。這稍微解釋下為社么不是算到i而是算到i的平方根窑业,因?yàn)槿绻钡絠的平方根都不能整除的話,那后面的數(shù)字也都不能整除枕屉,所以不必要都驗(yàn)證常柄,這樣能在算法上節(jié)約很多計(jì)算資源,使計(jì)算過程快很多搀擂。
以上代碼是計(jì)算300萬以內(nèi)的素?cái)?shù)列表西潘,我們先使用Python原生方式來執(zhí)行看一下需要多少時(shí)間。
time python primes.py
real 0m12.171s
user 0m11.828s
sys 0m0.344s
大概需要12秒多的時(shí)間哨颂,我們下一步要直接使用Cython來運(yùn)行一下這個(gè)Python代碼喷市,看看能快多少?
直接使用Cython來運(yùn)行Python
在上面的Python原生代碼執(zhí)行中威恼,我們已經(jīng)得到了一個(gè)時(shí)間品姓,在這一步我們要直接使用Cython直接運(yùn)行不加修改的Python代碼看看能提高多少寝并。在這一步之前我們要先安裝Cython:
pip install cython
使用Cython的步驟大概是三步:
- 把primes.py改為primes.pyx
from math import sqrt
def primes(n):
results = [1,]
for i in range(2, n):
for j in range(2, int(sqrt(i))):
if i % j == 0:
break
else:
results.append(i)
return results
def main():
primes(3000000)
if __name__ == '__main__':
main()
- 增加一個(gè)setup.py文件
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize('primes.pyx')
)
- 把Python編譯為二進(jìn)制代碼,
python setup.py build_ext --inplace
這一步會(huì)產(chǎn)生一些c源文件和編譯產(chǎn)生的動(dòng)態(tài)鏈接庫文件腹备。
ls
build primes.c primes.cpython-38-x86_64-linux-gnu.so primes.py primes.pyx setup.py
具體步驟會(huì)在后面解釋的衬潦。
- 運(yùn)行
time python -c "import primes; primes.main()"
real 0m7.501s
user 0m7.281s
sys 0m0.234s
在以上步驟中,有三個(gè)重要的過程
- 把pyx文件編譯成調(diào)用了Python源碼的C/C++代碼primes.c
- 把C代碼編譯成動(dòng)態(tài)鏈接庫primes.cpython-38-x86_64-linux-gnu.so
- 使用Python直接調(diào)用動(dòng)態(tài)鏈接庫植酥。
由以上的步驟的執(zhí)行結(jié)果來看镀岛,并沒有提高太多,只大概提交了一倍的速度友驮,這是因?yàn)镻ython的運(yùn)行速度慢除了因?yàn)槭墙忉寛?zhí)行以外還有一個(gè)最重要的原因是Python是動(dòng)態(tài)類型語言漂羊,每個(gè)變量在運(yùn)行前是不直到類型是什么的,所以即便編譯為二進(jìn)制代碼同樣速度不會(huì)太快卸留,這時(shí)候我們需要深度使用Cython來給Python提速了拨与,就是使用Cython來指定Python的數(shù)據(jù)類型。
使用Cython改進(jìn)的靜態(tài)類型指定
這一步不同就是primes.pyx文件和以上的文件不同艾猜,我們?cè)谄渲屑尤腩愋椭付ǖ拇a:
from math import sqrt
def primes(int n):
cdef int i, j
results = [1,]
for i in range(2, n):
for j in range(2, int(sqrt(i))):
if i % j == 0:
break
else:
results.append(i)
return results
def main():
primes(3000000)
if __name__ == '__main__':
main()
其中的代碼只有兩處不同:
- 函數(shù)參數(shù)的類型指定:int n
- 函數(shù)中使用最頻繁的兩個(gè)變量的類型指定:cdef int i, j
再運(yùn)行以上的相同步驟得到這次的運(yùn)行結(jié)果:
time python -c "import primes; primes.main()"
real 0m0.799s
user 0m0.734s
sys 0m0.063s
速度大概是原生Python的15倍左右,這只是在把Python代碼中常用的幾個(gè)變量改為靜態(tài)類型的情況下捻悯,如果把更多的變量和函數(shù)的返回值等都改為Cython的靜態(tài)類型后匆赃,性能一般能提升到原來的20-30倍。
后記
Python是一個(gè)很好用的語言今缚,效率問題大多數(shù)情況下可以通過橫向增加計(jì)算資源或者其他方式來彌補(bǔ)效率的不足算柳。極少數(shù)情況下是需要使用一些手段來提高語言的運(yùn)行效率。除了以上介紹的cython以外還有其他方案:pypy和Shed Skin等解決方案姓言,在以后的文檔中會(huì)分享其他的解決方案瞬项。