Python性能分析

Python的高效體現(xiàn)在它的開發(fā)效率,完善的類庫支持,特別是這幾年在數(shù)據(jù)科學中的流行,使得Python開始在各種語言排行榜獨占鰲頭,但是Python代碼運行效率低這一點也一直被詬病,要想讓Python高效運行,掌握一套性能分析的方法和工具,就顯得很重要。

一般通過profiling找到代碼瓶頸,然后針對這個瓶頸代碼進行優(yōu)化,從而可以到達事半功倍的效果,即用最少的工作量來獲得最大的性能的提升,這在任何場景下都是最優(yōu)的選擇。任何可衡量的資源都可以進行profiling,除了CPU,內(nèi)存還包括網(wǎng)絡帶寬,磁盤I/O。

profiling的目的就是通過對系統(tǒng)進行分析,找出哪里比較慢,哪里消耗內(nèi)存比較多,哪里會引起更多的磁盤I/O或者網(wǎng)絡I/O。但是profiling通常會增加代碼的額外開銷,這種開銷有時候是非常大,會10x甚至100x的降低代碼運行效率。所以profiling一般不能直接針對線上的代碼進行,一般是構(gòu)建一套類線上環(huán)境,或者把需要做profiling的代碼拿出來單獨進行分析。

Python常用的profiling方法:

Timing

最基礎的就是使用time.time()來計時,這個方法簡單有效,也許所有寫過Python代碼的人都用過。

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)建一個decorator使它用起來更方便。

import time
from functools import wraps
...
def simple_profiling(fn):
    @wraps(fn)  # 對外暴露調(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)
    ...

這個方法的優(yōu)點是簡單,額外開效非常低(大部分情況下可以忽略不計)。但缺點也很明顯,除了總用時,沒有任何其他信息。

Python的timeit模塊也提供了測量小段代碼執(zhí)行時間的方法。需要注意在使用timeit模塊時,GC會被臨時關閉,所以這個可能導致測試結(jié)果跟真實運行結(jié)果有差距。

python -m timeit [-n N] [-r N] [-s S] [-t] [-c] [-h] [statement ...]
-n : 執(zhí)行指定語句的次數(shù)
-r : 重復測量的次數(shù)(默認3次)
-s : 指定初始化代碼或構(gòu)建環(huán)境的導入語句
-t : 使用time.time() (Windows平臺以外的默認值)
-c : 使用time.clock() (Windows平臺默認值)

舉一個-s的例子,假設我們在test.py里面定義了一個函數(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工具,用來統(tǒng)計腳本執(zhí)行的時間,它的特點是把運行時間分成real,user,sys三部分。

$ time python test_time.py

real    0m0.069s
user    0m0.038s
sys     0m0.033s

cProfile

cProfile是Python標準庫中的一個模塊,它可以非常仔細地分析代碼執(zhí)行過程中所有函數(shù)調(diào)用的用時和次數(shù)。cProfile最簡單的用法是用cProfile.run來執(zhí)行一段代碼,或是用python -m cProfile myscript.py來執(zhí)行一個腳本。例如

$ 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}

整個輸出結(jié)果給出了每一個函數(shù)的耗時信息。每個列的字段含義如下:

  • ncalls: 函數(shù)被調(diào)用次數(shù)
  • tottime: 函數(shù)總耗時,子函數(shù)的執(zhí)行時間不計算在內(nèi)
  • percall: tottime / ncalls
  • cumtime: 函數(shù)加上其所有子函數(shù)的總耗時
  • percall: cumtime / ncalls

這個例子比較簡單,所有輸出很少,真實的例子估計輸出會非常多,我們可以把結(jié)果通過 -o profile.stats 把結(jié)果保存到文件,然后通過強大的pstats模塊來進行分析。關于pstats的具體使用可以看官方文檔。
關于使用cProfile進行性能分析時,推薦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,因為是對長期運行的代碼進行profiling,所以這里把immediate設置成True,表示代碼執(zhí)行完立即輸出,而不是等程序結(jié)束。profile還有很多其他參數(shù),詳細請看github上源碼注釋profilehooks.py

如果覺得pstats使用不方便,還可以使用一些圖形化工具,比如gprof2dotRunSnakeRun來可視化分析cProfile的診斷結(jié)果。這兩個工具推薦一起使用,RunSnakeRun能很快發(fā)現(xiàn)那些函數(shù)執(zhí)行時間占比比較大,然后通過gprof2dot畫的函數(shù)調(diào)用圖來具體分析。

Line Profiler

我們通過cProfile定位到了具體的耗時函數(shù),下面就需要具體定位瓶頸出在哪行代碼,這個時候就到了Line Profiler出場的時候了。與cProfile相比,Line Profiler的結(jié)果更加直觀,它可以告訴你一個函數(shù)中每一行的耗時。Line Profiler并不在標準庫中,需要用pip來安裝。

pip install line_profiler

line_profiler的使用特別簡單,在需要監(jiān)控的函數(shù)前面加上@profile裝飾器。然后用它提供的 kernprof -l -v source_code.py 進行診斷。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 雖然運行速度慢是 Python 與生俱來的特點,大多數(shù)時候我們用 Python 就意味著放棄對性能的追求。但是,就...
    selfboot閱讀 2,037評論 2 28
  • 【編者按】本文作者為 Bryan Helmig,主要介紹 Python 應用性能分析的三種進階方案。文章系國內(nèi) I...
    OneAPM_Official閱讀 1,299評論 0 1
  • 性能分析和調(diào)優(yōu)工具簡介<a id="sec-1" name="sec-1"></a> 總會遇到一個時候你會想提高程...
    Nisen閱讀 1,056評論 0 3
  • 作者: Nisen 總會遇到一個時候你會想提高程序執(zhí)行效率,想看看哪部分耗時長成為瓶頸,想知道程序運行時內(nèi)存和CP...
    PyChina閱讀 715評論 0 2
  • 使用性能計時器 在Nuke中打開性能計時器時,就可以通過Python讀取性能信息。就能知道每個節(jié)點的運算時間。在調(diào)...
    N景波閱讀 1,284評論 0 0

友情鏈接更多精彩內(nèi)容