Python性能分析指南

雖然你所寫的每一個Python程序並不老是須要嚴密的性能分析,可是當這樣的問題出現時,若是能知道Python生態系統中的許多種工具,這樣老是可讓人安心的。python

分析一個程序的性能能夠歸結爲回答4個基本的問題:git

1.它運行的有多塊?
2.那裏是速度的瓶頸?
3.它使用了多少內存?
4.哪裏發生了內存泄漏?github

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

使用time工具粗糙定時

首先,咱們可使用快速然而粗糙的工具:古老的unix工具time,來爲咱們的代碼檢測運行時間。app

1 $ time python yourprogram.py
2  
3 real    0m1.028s
4 user    0m0.001s
5 sys     0m0.003s

上面三個輸入變量的意義在文章 stackoverflow article 中有詳細介紹。簡單的說:函數

  • real - 表示實際的程序運行時間
  • user - 表示程序在用戶態的cpu總時間
  • sys - 表示在內核態的cpu總時間

經過sysuser時間的求和,你能夠直觀的獲得系統上沒有其餘程序運行時你的程序運行所須要的CPU週期。工具

sysuser時間之和遠遠少於real時間,那麼你能夠猜想你的程序的主要性能問題極可能與IO等待相關。性能

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

咱們的下一個技術涉及訪問細粒度計時信息的直接代碼指令。這是一小段代碼,我發現使用專門的計時測量是很是重要的:優化

timer.pyui

01 import time
02  
03 class Timer(object):
04     def __init__(self, verbose=False):
05         self.verbose = verbose
06  
07     def __enter__(self):
08         self.start = time.time()
09         return self
10  
11     def __exit__(self*args):
12         self.end = time.time()
13         self.secs = self.end - self.start
14         self.msecs = self.secs * 1000  # millisecs
15         if self.verbose:
16             print 'elapsed time: %f ms' % self.msecs

爲了使用它,你須要用Python的with關鍵字和Timer上下文管理器包裝想要計時的代碼塊。它將會在你的代碼塊開始執行的時候啓動計時器,在你的代碼塊結束的時候中止計時器。

這是一個使用上述代碼片斷的例子:

01 from timer import Timer
02 from redis import Redis
03 rdb = Redis()
04  
05 with Timer() as t:
06     rdb.lpush("foo""bar")
07 print "=> elasped lpush: %s s" % t.secs
08  
09 with Timer as t:
10     rdb.lpop("foo")
11 print "=> elasped lpop: %s s" % t.secs

我常常將這些計時器的輸出記錄到文件中,這樣就能夠觀察個人程序的性能如何隨着時間進化。

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

Robert Kern有一個稱做line_profiler的不錯的項目,我常用它查看個人腳步中每行代碼多快多頻繁的被執行。

想要使用它,你須要經過pip安裝該python包:

1 $ pip install line_profiler

一旦安裝完成,你將會使用一個稱作「line_profiler」的新模組和一個「kernprof.py」可執行腳本。

想要使用該工具,首先修改你的源代碼,在想要測量的函數上裝飾@profile裝飾器。不要擔憂,你不須要導入任何模組。kernprof.py腳本將會在執行的時候將它自動地注入到你的腳步的運行時。

primes.py

01 @profile
02 defprimes(n):
03     if n==2:
04         return [2]
05     elif n<2:
06         return []
07     s=range(3,n+1,2)
08     mroot = ** 0.5
09     half=(n+1)/2-1
10     i=0
11     m=3
12     while m <= mroot:
13         if s[i]:
14             j=(m*m-3)/2
15             s[j]=0
16             while j<half:
17                 s[j]=0
18                 j+=m
19         i=i+1
20         m=2*i+3
21     return [2]+[x for in if x]
22 primes(100)

一旦你已經設置好了@profile裝飾器,使用kernprof.py執行你的腳步。

1 $ kernprof.py --v fib.py

-l選項通知kernprof注入@profile裝飾器到你的腳步的內建函數,-v選項通知kernprof在腳本執行完畢的時候顯示計時信息。上述腳本的輸出看起來像這樣:

01 Wrote profile results to primes.py.lprof
02 Timer unit: 1e-06 s
03  
04 File: primes.py
05 Function: primes at line 2
06 Total time: 0.00019 s
07  
08 Line #      Hits         Time  Per Hit   % Time  Line Contents
09 ==============================================================
10      2                                           @profile
11      3                                           def primes(n):
12      4         1            2      2.0      1.1      if n==2:
13      5                                                   return [2]
14      6         1            1      1.0      0.5      elif n<2:
15      7                                                   return []
16      8         1            4      4.0      2.1      s=range(3,n+1,2)
17      9         1           10     10.0      5.3      mroot = n ** 0.5
18     10         1            2      2.0      1.1      half=(n+1)/2-1
19     11         1            1      1.0      0.5      i=0
20     12         1            1      1.0      0.5      m=3
21     13         5            7      1.4      3.7      while m <= mroot:
22     14         4            4      1.0      2.1          if s[i]:
23     15         3            4      1.3      2.1              j=(m*m-3)/2
24     16         3            4      1.3      2.1              s[j]=0
25     17        31           31      1.0     16.3              while j<half:
26     18        28           28      1.0     14.7                  s[j]=0
27     19        28           29      1.0     15.3                  j+=m
28     20         4            4      1.0      2.1          i=i+1
29     21         4            4      1.0      2.1          m=2*i+3
30     22        50           54      1.1     28.4      return [2]+[x for in if x]

尋找具備高Hits值或高Time值的行。這些就是能夠經過優化帶來最大改善的地方。

程序使用了多少內存?

如今咱們對計時有了較好的理解,那麼讓咱們繼續弄清楚程序使用了多少內存。咱們很幸運,Fabian Pedregosa模仿Robert Kern的line_profiler實現了一個不錯的內存分析器

首先使用pip安裝:

1 $ pip install -U memory_profiler
2 $ pip install psutil

(這裏建議安裝psutil包,由於它能夠大大改善memory_profiler的性能)。

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

1 @profile
2 defprimes(n):
3     ...
4     ...

想要觀察你的函數使用了多少內存,像下面這樣執行:

1 $ python -m memory_profiler primes.py

一旦程序退出,你將會看到看起來像這樣的輸出:

01 Filename: primes.py
02  
03 Line #    Mem usage  Increment   Line Contents
04 ==============================================
05      2                           @profile
06      3    7.9219 MB  0.0000 MB   defprimes(n):
07      4    7.9219 MB  0.0000 MB       if n==2:
08      5                                   return [2]
09      6    7.9219 MB  0.0000 MB       elif n<2:
10      7                                   return []
11      8    7.9219 MB  0.0000 MB       s=range(3,n+1,2)
12      9    7.9258 MB  0.0039 MB       mroot = ** 0.5
13     10    7.9258 MB  0.0000 MB       half=(n+1)/2-1
14     11    7.9258 MB  0.0000 MB       i=0
15     12    7.9258 MB  0.0000 MB       m=3
16     13    7.9297 MB  0.0039 MB       while m <= mroot:
17     14    7.9297 MB  0.0000 MB           if s[i]:
18     15    7.9297 MB  0.0000 MB               j=(m*m-3)/2
19     16    7.9258 MB -0.0039 MB               s[j]=0
20     17    7.9297 MB  0.0039 MB               while j<half:
21     18    7.9297 MB  0.0000 MB                   s[j]=0
22     19    7.9297 MB  0.0000 MB                   j+=m
23     20    7.9297 MB  0.0000 MB           i=i+1
24     21    7.9297 MB  0.0000 MB           m=2*i+3
25     22    7.9297 MB  0.0000 MB       return [2]+[x for in if x]

line_profiler和memory_profiler的IPython快捷方式

memory_profiler和line_profiler有一個不爲人知的小竅門,二者都有在IPython中的快捷命令。你須要作的就是在IPython會話中輸入如下內容:

1 %load_ext memory_profiler
2 %load_ext line_profiler

在這樣作的時候你須要訪問魔法命令%lprun和%mprun,它們的行爲相似於他們的命令行形式。主要區別是你不須要使用@profiledecorator來修飾你要分析的函數。只須要在IPython會話中像先前同樣直接運行分析:

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

這樣能夠節省你不少時間和精力,由於你的源代碼不須要爲使用這些分析命令而進行修改。

內存泄漏在哪裏?

cPython解釋器使用引用計數作爲記錄內存使用的主要方法。這意味着每一個對象包含一個計數器,當某處對該對象的引用被存儲時計數器增長,當引用被刪除時計數器遞減。當計數器到達零時,cPython解釋器就知道該對象再也不被使用,因此刪除對象,釋放佔用的內存。

若是程序中再也不被使用的對象的引用一直被佔有,那麼就常常發生內存泄漏。

查找這種「內存泄漏」最快的方式是使用Marius Gedminas編寫的objgraph,這是一個極好的工具。該工具容許你查看內存中對象的數量,定位含有該對象的引用的全部代碼的位置。

一開始,首先安裝objgraph:

1 pip install objgraph

一旦你已經安裝了這個工具,在你的代碼中插入一行聲明調用調試器:

1 import pdb; pdb.set_trace()
最廣泛的對象是哪些?

在運行的時候,你能夠經過執行下述指令查看程序中前20個最廣泛的對象:

01 (pdb) import objgraph
02 (pdb) objgraph.show_most_common_types()
03  
04 MyBigFatObject             20000
05 tuple                      16938
06 function                   4310
07 dict                       2790
08 wrapper_descriptor         1181
09 builtin_function_or_method 934
10 weakref                    764
11 list                       634
12 method_descriptor          507
13 getset_descriptor          451
14 type                       439
哪些對象已經被添加或刪除?

咱們也能夠查看兩個時間點之間那些對象已經被添加或刪除:

01 (pdb) import objgraph
02 (pdb) objgraph.show_growth()
03 .
04 .
05 .
06 (pdb) objgraph.show_growth()   # this only shows objects that has been added or deleted since last show_growth() call
07  
08 traceback                4        +2
09 KeyboardInterrupt        1        +1
10 frame                   24        +1
11 list                   667        +1
12 tuple                16969        +1
誰引用着泄漏的對象?

繼續,你還能夠查看哪裏包含給定對象的引用。讓咱們如下述簡單的程序作爲一個例子:

1 = [1]
2 = [x, [x], {"a":x}]
3 import pdb; pdb.set_trace()

想要看看哪裏包含變量x的引用,執行objgraph.show_backref()函數:

1 (pdb) import objgraph
2 (pdb) objgraph.show_backref([x], filename="/tmp/backrefs.png")

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

back refrences

最下面有紅字的盒子是咱們感興趣的對象。咱們能夠看到,它被符號x引用了一次,被列表y引用了三次。若是是x引發了一個內存泄漏,咱們可使用這個方法,經過跟蹤它的全部引用,來檢查爲何它沒有自動的被釋放。

回顧一下,objgraph 使咱們能夠:

  • 顯示佔據python程序內存的頭N個對象
  • 顯示一段時間之後哪些對象被刪除活增長了
  • 在咱們的腳本中顯示某個給定對象的全部引用

努力與精度

在本帖中,我給你顯示了怎樣用幾個工具來分析python程序的性能。經過這些工具與技術的武裝,你能夠得到全部須要的信息,來跟蹤一個python程序中大多數的內存泄漏,以及識別出其速度瓶頸。

對許多其餘觀點來講,運行一次性能分析就意味着在努力目標與事實精度之間作出平衡。若是感到困惑,那麼就實現能適應你目前需求的最簡單的解決方案。

參考
相關文章
相關標籤/搜索