Python的高效體現(xiàn)在它的開(kāi)發(fā)效率励饵,完善的類(lèi)庫(kù)支持,特別是這幾年在數(shù)據(jù)科學(xué)中的流行,使得Python開(kāi)始在各種語(yǔ)言排行榜獨(dú)占鰲頭,但是Python代碼運(yùn)行效率低這一點(diǎn)也一直被詬病瓦糕,要想讓Python高效運(yùn)行,掌握一套性能分析的方法和工具腋么,就顯得很重要咕娄。
一般通過(guò)profiling找到代碼瓶頸,然后針對(duì)這個(gè)瓶頸代碼進(jìn)行優(yōu)化珊擂,從而可以到達(dá)事半功倍的效果圣勒,即用最少的工作量來(lái)獲得最大的性能的提升,這在任何場(chǎng)景下都是最優(yōu)的選擇摧扇。任何可衡量的資源都可以進(jìn)行profiling圣贸,除了CPU,內(nèi)存還包括網(wǎng)絡(luò)帶寬扛稽,磁盤(pán)I/O吁峻。
profiling的目的就是通過(guò)對(duì)系統(tǒng)進(jìn)行分析,找出哪里比較慢在张,哪里消耗內(nèi)存比較多用含,哪里會(huì)引起更多的磁盤(pán)I/O或者網(wǎng)絡(luò)I/O。但是profiling通常會(huì)增加代碼的額外開(kāi)銷(xiāo)帮匾,這種開(kāi)銷(xiāo)有時(shí)候是非常大啄骇,會(huì)10x甚至100x的降低代碼運(yùn)行效率。所以profiling一般不能直接針對(duì)線上的代碼進(jìn)行瘟斜,一般是構(gòu)建一套類(lèi)線上環(huán)境缸夹,或者把需要做profiling的代碼拿出來(lái)單獨(dú)進(jìn)行分析。
Python常用的profiling方法:
Timing
最基礎(chǔ)的就是使用time.time()來(lái)計(jì)時(shí)哼转,這個(gè)方法簡(jiǎn)單有效明未,也許所有寫(xiě)過(guò)Python代碼的人都用過(guò)。
import time
...
start_time = time.time()
output = foo(a, b, c)
end_time = time.time()
secs = end_time - start_time
print foo.func_name + " took", secs, "seconds"
我們可以創(chuàng)建一個(gè)decorator使它用起來(lái)更方便壹蔓。
import time
from functools import wraps
...
def simple_profiling(fn):
@wraps(fn) # 對(duì)外暴露調(diào)用裝飾器函數(shù)的函數(shù)名和docstring
def wrapped(*args, **kwargs):
t1 = time.time()
result = fn(*args, **kwargs)
t2 = time.time()
print (
"@simple_profiling:" + fn.func_name + " took " + str(t2 - t1) + " seconds")
return result
return wrapped
...
@simple_profiling
def foo(a, b, c)
...
這個(gè)方法的優(yōu)點(diǎn)是簡(jiǎn)單趟妥,額外開(kāi)效非常低(大部分情況下可以忽略不計(jì))。但缺點(diǎn)也很明顯佣蓉,除了總用時(shí)披摄,沒(méi)有任何其他信息。
Python的timeit模塊也提供了測(cè)量小段代碼執(zhí)行時(shí)間的方法勇凭。需要注意在使用timeit模塊時(shí)疚膊,GC會(huì)被臨時(shí)關(guān)閉,所以這個(gè)可能導(dǎo)致測(cè)試結(jié)果跟真實(shí)運(yùn)行結(jié)果有差距虾标。
python -m timeit [-n N] [-r N] [-s S] [-t] [-c] [-h] [statement ...]
-n : 執(zhí)行指定語(yǔ)句的次數(shù)
-r : 重復(fù)測(cè)量的次數(shù)(默認(rèn)3次)
-s : 指定初始化代碼或構(gòu)建環(huán)境的導(dǎo)入語(yǔ)句
-t : 使用time.time() (Windows平臺(tái)以外的默認(rèn)值)
-c : 使用time.clock() (Windows平臺(tái)默認(rèn)值)
舉一個(gè)-s的例子寓盗,假設(shè)我們?cè)趖est.py里面定義了一個(gè)函數(shù)foo
python -m timeit -n 5 -r 5 -s "import test" "test.foo(a,b,c)"
5 loops, best of 5: 28.6 usec per loop
timtie還可以在IPython環(huán)境下使用:
In [6]: timeit '"-".join(map(str, xrange(100)))'
100000000 loops, best of 3: 9.75 ns per loop
另外unix系統(tǒng)也提供time工具,用來(lái)統(tǒng)計(jì)腳本執(zhí)行的時(shí)間,它的特點(diǎn)是把運(yùn)行時(shí)間分成real,user,sys三部分傀蚌。
$ time python test_time.py
real 0m0.069s
user 0m0.038s
sys 0m0.033s
cProfile
cProfile是Python標(biāo)準(zhǔn)庫(kù)中的一個(gè)模塊基显,它可以非常仔細(xì)地分析代碼執(zhí)行過(guò)程中所有函數(shù)調(diào)用的用時(shí)和次數(shù)。cProfile最簡(jiǎn)單的用法是用cProfile.run來(lái)執(zhí)行一段代碼善炫,或是用python -m cProfile myscript.py來(lái)執(zhí)行一個(gè)腳本撩幽。例如
$ python -m cProfile -s cumulative -o profile.stats test_time.py
5 function calls in 0.232 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.232 0.232 test_time.py:3(<module>)
1 0.018 0.018 0.231 0.231 test_time.py:3(foo)
1 0.177 0.177 0.177 0.177 {map}
1 0.036 0.036 0.036 0.036 {method 'join' of 'str' objects}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
整個(gè)輸出結(jié)果給出了每一個(gè)函數(shù)的耗時(shí)信息。每個(gè)列的字段含義如下:
- ncalls: 函數(shù)被調(diào)用次數(shù)
- tottime: 函數(shù)總耗時(shí)箩艺,子函數(shù)的執(zhí)行時(shí)間不計(jì)算在內(nèi)
- percall: tottime / ncalls
- cumtime: 函數(shù)加上其所有子函數(shù)的總耗時(shí)
- percall: cumtime / ncalls
這個(gè)例子比較簡(jiǎn)單窜醉,所有輸出很少,真實(shí)的例子估計(jì)輸出會(huì)非常多艺谆,我們可以把結(jié)果通過(guò) -o profile.stats 把結(jié)果保存到文件榨惰,然后通過(guò)強(qiáng)大的pstats模塊來(lái)進(jìn)行分析。關(guān)于pstats的具體使用可以看官方文檔擂涛。
關(guān)于使用cProfile進(jìn)行性能分析時(shí)读串,推薦profilehooks,使用方法如下:
from profilehooks import profile
class SampleClass(ParentClass):
@profile(filename="/tmp/SampleClass_do_something.stats", immediate=True, stdout=False)
def do_something(self):
...
這里把輸出結(jié)果不輸出到stdout而是直接保存到/tmp/SampleClass_do_something.stats撒妈,因?yàn)槭菍?duì)長(zhǎng)期運(yùn)行的代碼進(jìn)行profiling恢暖,所以這里把immediate設(shè)置成True,表示代碼執(zhí)行完立即輸出狰右,而不是等程序結(jié)束杰捂。profile還有很多其他參數(shù),詳細(xì)請(qǐng)看github上源碼注釋profilehooks.py
如果覺(jué)得pstats使用不方便棋蚌,還可以使用一些圖形化工具嫁佳,比如gprof2dot和RunSnakeRun來(lái)可視化分析cProfile的診斷結(jié)果谷暮。這兩個(gè)工具推薦一起使用蒿往,RunSnakeRun能很快發(fā)現(xiàn)那些函數(shù)執(zhí)行時(shí)間占比比較大,然后通過(guò)gprof2dot畫(huà)的函數(shù)調(diào)用圖來(lái)具體分析湿弦。
Line Profiler
我們通過(guò)cProfile定位到了具體的耗時(shí)函數(shù)瓤漏,下面就需要具體定位瓶頸出在哪行代碼,這個(gè)時(shí)候就到了Line Profiler出場(chǎng)的時(shí)候了颊埃。與cProfile相比蔬充,Line Profiler的結(jié)果更加直觀,它可以告訴你一個(gè)函數(shù)中每一行的耗時(shí)班利。Line Profiler并不在標(biāo)準(zhǔn)庫(kù)中饥漫,需要用pip來(lái)安裝。
pip install line_profiler
line_profiler的使用特別簡(jiǎn)單罗标,在需要監(jiān)控的函數(shù)前面加上@profile裝飾器庸队。然后用它提供的 kernprof -l -v source_code.py 進(jìn)行診斷积蜻。