Python效能分析指南

類別: IT
標籤: python

雖然你所寫的每個Python程式並不總是需要嚴密的效能分析,但是當這樣的問題出現時,如果能知道Python生態系統中的許多種工具,這樣總是可以讓人安心的。

分析一個程式的效能可以歸結為回答4個基本的問題:

1.它執行的有多塊?
2.那裡是速度的瓶頸?
3.它使用了多少記憶體?
4.哪裡發生了記憶體洩漏?

下面,我們將用一些很酷的工具,深入細節的回答這些問題。

使用time工具粗糙定時

首先,我們可以使用快速然而粗糙的工具:古老的unix工具time,來為我們的程式碼檢測執行時間。

$ time python yourprogram.pyreal    0m1.028suser    0m0.001ssys     0m0.003s
上面三個輸入變數的意義在文章 stackoverflow article 中有詳細介紹。簡單的說:
  • real - 表示實際的程式執行時間
  • user - 表示程式在使用者態的cpu總時間
  • sys - 表示在核心態的cpu總時間

通過sysuser時間的求和,你可以直觀的得到系統上沒有其他程式執行時你的程式執行所需要的CPU週期。

sysuser時間之和遠遠少於real時間,那麼你可以猜測你的程式的主要效能問題很可能與IO等待相關。

使用計時上下文管理器進行細粒度計時

我們的下一個技術涉及訪問細粒度計時資訊的直接程式碼指令。這是一小段程式碼,我發現使用專門的計時測量是非常重要的:

timer.py

import timeclass Timer(object):    def __init__(self, verbose=False):        self.verbose = verbose    def __enter__(self):        self.start = time.time()        return self    def __exit__(self, *args):        self.end = time.time()        self.secs = self.end - self.start        self.msecs = self.secs * 1000  # millisecs        if self.verbose:            print 'elapsed time: %f ms' % self.msecs

為了使用它,你需要用Python的with關鍵字和Timer上下文管理器包裝想要計時的程式碼塊。它將會在你的程式碼塊開始執行的時候啟動計時器,在你的程式碼塊結束的時候停止計時器。

這是一個使用上述程式碼片段的例子:

from timer import Timerfrom redis import Redisrdb = Redis()with Timer() as t:    rdb.lpush("foo", "bar")print "=> elasped lpush: %s s" % t.secswith Timer as t:    rdb.lpop("foo")print "=> elasped lpop: %s s" % t.secs

我經常將這些計時器的輸出記錄到檔案中,這樣就可以觀察我的程式的效能如何隨著時間進化。

使用分析器逐行統計時間和執行頻率

Robert Kern有一個稱作line_profiler的不錯的專案,我經常使用它檢視我的腳步中每行程式碼多快多頻繁的被執行。

想要使用它,你需要通過pip安裝該python包:

$ pip install line_profiler

一旦安裝完成,你將會使用一個稱做“line_profiler”的新模組和一個“kernprof.py”可執行指令碼。

想要使用該工具,首先修改你的原始碼,在想要測量的函式上裝飾@profile裝飾器。不要擔心,你不需要匯入任何模組。kernprof.py指令碼將會在執行的時候將它自動地注入到你的腳步的執行時。

primes.py

@profiledef primes(n):     if n==2:        return [2]    elif n<2:        return []    s=range(3,n+1,2)    mroot = n ** 0.5    half=(n+1)/2-1    i=0    m=3    while m <= mroot:        if s[i]:            j=(m*m-3)/2            s[j]=0            while j<half:                s[j]=0                j+=m        i=i+1        m=2*i+3    return [2]+[x for x in s if x]primes(100)
一旦你已經設定好了@profile裝飾器,使用kernprof.py執行你的腳步。
$ kernprof.py -l -v fib.py
-l選項通知kernprof注入@profile裝飾器到你的腳步的內建函式,-v選項通知kernprof在指令碼執行完畢的時候顯示計時資訊。上述指令碼的輸出看起來像這樣:
Wrote profile results to primes.py.lprofTimer unit: 1e-06 sFile: primes.pyFunction: primes at line 2Total time: 0.00019 sLine #      Hits         Time  Per Hit   % Time  Line Contents==============================================================     2                                           @profile     3                                           def primes(n):      4         1            2      2.0      1.1      if n==2:     5                                                   return [2]     6         1            1      1.0      0.5      elif n<2:     7                                                   return []     8         1            4      4.0      2.1      s=range(3,n+1,2)     9         1           10     10.0      5.3      mroot = n ** 0.5    10         1            2      2.0      1.1      half=(n+1)/2-1    11         1            1      1.0      0.5      i=0    12         1            1      1.0      0.5      m=3    13         5            7      1.4      3.7      while m <= mroot:    14         4            4      1.0      2.1          if s[i]:    15         3            4      1.3      2.1              j=(m*m-3)/2    16         3            4      1.3      2.1              s[j]=0    17        31           31      1.0     16.3              while j<half:    18        28           28      1.0     14.7                  s[j]=0    19        28           29      1.0     15.3                  j+=m    20         4            4      1.0      2.1          i=i+1    21         4            4      1.0      2.1          m=2*i+3    22        50           54      1.1     28.4      return [2]+[x for x in s if x]

尋找具有高Hits值或高Time值的行。這些就是可以通過優化帶來最大改善的地方。

程式使用了多少記憶體?

現在我們對計時有了較好的理解,那麼讓我們繼續弄清楚程式使用了多少記憶體。我們很幸運,Fabian Pedregosa模仿Robert Kern的line_profiler實現了一個不錯的記憶體分析器

首先使用pip安裝:

$ pip install -U memory_profiler$ pip install psutil

(這裡建議安裝psutil包,因為它可以大大改善memory_profiler的效能)。

就像line_profiler,memory_profiler也需要在感興趣的函式上面裝飾@profile裝飾器:

@profiledef primes(n):     ...    ...
想要觀察你的函式使用了多少記憶體,像下面這樣執行:
$ python -m memory_profiler primes.py
一旦程式退出,你將會看到看起來像這樣的輸出:
Filename: primes.pyLine #    Mem usage  Increment   Line Contents==============================================     2                           @profile     3    7.9219 MB  0.0000 MB   def primes(n):      4    7.9219 MB  0.0000 MB       if n==2:     5                                   return [2]     6    7.9219 MB  0.0000 MB       elif n<2:     7                                   return []     8    7.9219 MB  0.0000 MB       s=range(3,n+1,2)     9    7.9258 MB  0.0039 MB       mroot = n ** 0.5    10    7.9258 MB  0.0000 MB       half=(n+1)/2-1    11    7.9258 MB  0.0000 MB       i=0    12    7.9258 MB  0.0000 MB       m=3    13    7.9297 MB  0.0039 MB       while m <= mroot:    14    7.9297 MB  0.0000 MB           if s[i]:    15    7.9297 MB  0.0000 MB               j=(m*m-3)/2    16    7.9258 MB -0.0039 MB               s[j]=0    17    7.9297 MB  0.0039 MB               while j<half:    18    7.9297 MB  0.0000 MB                   s[j]=0    19    7.9297 MB  0.0000 MB                   j+=m    20    7.9297 MB  0.0000 MB           i=i+1    21    7.9297 MB  0.0000 MB           m=2*i+3    22    7.9297 MB  0.0000 MB       return [2]+[x for x in s if x]

line_profiler和memory_profiler的IPython快捷方式

memory_profiler和line_profiler有一個鮮為人知的小竅門,兩者都有在IPython中的快捷命令。你需要做的就是在IPython會話中輸入以下內容:

%load_ext memory_profiler%load_ext line_profiler

在這樣做的時候你需要訪問魔法命令%lprun和%mprun,它們的行為類似於他們的命令列形式。主要區別是你不需要使用@profiledecorator來修飾你要分析的函式。只需要在IPython會話中像先前一樣直接執行分析:

In [1]: from primes import primesIn [2]: %mprun -f primes primes(1000)In [3]: %lprun -f primes primes(1000)

這樣可以節省你很多時間和精力,因為你的原始碼不需要為使用這些分析命令而進行修改。

記憶體洩漏在哪裡?

cPython直譯器使用引用計數做為記錄記憶體使用的主要方法。這意味著每個物件包含一個計數器,當某處對該物件的引用被儲存時計數器增加,當引用被刪除時計數器遞減。當計數器到達零時,cPython直譯器就知道該物件不再被使用,所以刪除物件,釋放佔用的記憶體。

如果程式中不再被使用的物件的引用一直被佔有,那麼就經常發生記憶體洩漏。

查詢這種“記憶體洩漏”最快的方式是使用Marius Gedminas編寫的objgraph,這是一個極好的工具。該工具允許你檢視記憶體中物件的數量,定位含有該物件的引用的所有程式碼的位置。

一開始,首先安裝objgraph:

pip install objgraph
一旦你已經安裝了這個工具,在你的程式碼中插入一行宣告呼叫偵錯程式:
import pdb; pdb.set_trace()
最普遍的物件是哪些?

在執行的時候,你可以通過執行下述指令檢視程式中前20個最普遍的物件:

(pdb) import objgraph(pdb) objgraph.show_most_common_types()MyBigFatObject             20000tuple                      16938function                   4310dict                       2790wrapper_descriptor         1181builtin_function_or_method 934weakref                    764list                       634method_descriptor          507getset_descriptor          451type                       439
哪些物件已經被新增或刪除?

我們也可以檢視兩個時間點之間那些物件已經被新增或刪除:

(pdb) import objgraph(pdb) objgraph.show_growth()...(pdb) objgraph.show_growth()   # this only shows objects that has been added or deleted since last show_growth() calltraceback                4        +2KeyboardInterrupt        1        +1frame                   24        +1list                   667        +1tuple                16969        +1
誰引用著洩漏的物件?

繼續,你還可以檢視哪裡包含給定物件的引用。讓我們以下述簡單的程式做為一個例子:

x = [1]y = [x, [x], {"a":x}]import pdb; pdb.set_trace()
想要看看哪裡包含變數x的引用,執行objgraph.show_backref()函式:
(pdb) import objgraph(pdb) objgraph.show_backref([x], filename="/tmp/backrefs.png")

該命令的輸出應該是一副PNG影象,儲存在/tmp/backrefs.png,它看起來是像這樣:

back refrences

最下面有紅字的盒子是我們感興趣的物件。我們可以看到,它被符號x引用了一次,被列表y引用了三次。如果是x引起了一個記憶體洩漏,我們可以使用這個方法,通過跟蹤它的所有引用,來檢查為什麼它沒有自動的被釋放。

回顧一下,objgraph 使我們可以:

  • 顯示佔據python程式記憶體的頭N個物件
  • 顯示一段時間以後哪些物件被刪除活增加了
  • 在我們的指令碼中顯示某個給定物件的所有引用

努力與精度

在本帖中,我給你顯示了怎樣用幾個工具來分析python程式的效能。通過這些工具與技術的武裝,你可以獲得所有需要的資訊,來跟蹤一個python程式中大多數的記憶體洩漏,以及識別出其速度瓶頸。

對許多其他觀點來說,執行一次效能分析就意味著在努力目標與事實精度之間做出平衡。如果感到困惑,那麼就實現能適應你目前需求的最簡單的解決方案。

參考
  • stack overflow - time explained(堆疊溢位 - 時間解釋)
  • line_profiler(線性分析器)
  • memory_profiler(記憶體分析器)
  • objgraph(物件圖)










Python效能分析指南原文請看這裡

推薦文章