雖然你所寫的每一個Python程序並不老是須要嚴密的性能分析,可是當這樣的問題出現時,若是能知道Python生態系統中的許多種工具,這樣老是可讓人安心的。python
分析一個程序的性能能夠歸結爲回答4個基本的問題:git
1.它運行的有多塊?
2.那裏是速度的瓶頸?
3.它使用了多少內存?
4.哪裏發生了內存泄漏?github
下面,咱們將用一些很酷的工具,深刻細節的回答這些問題。redis
首先,咱們可使用快速然而粗糙的工具:古老的unix工具time,來爲咱們的代碼檢測運行時間。app
1 |
$ time python yourprogram.py |
2 |
3 |
real 0m1 . 028s |
4 |
user 0m0 . 001s |
5 |
sys 0m0 . 003s |
上面三個輸入變量的意義在文章 stackoverflow article 中有詳細介紹。簡單的說:函數
經過sys和user時間的求和,你能夠直觀的獲得系統上沒有其餘程序運行時你的程序運行所須要的CPU週期。工具
若sys和user時間之和遠遠少於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 |
def primes(n): |
03 |
if n = = 2 : |
04 |
return [ 2 ] |
05 |
elif n< 2 : |
06 |
return [] |
07 |
s = range ( 3 ,n + 1 , 2 ) |
08 |
mroot = n * * 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 x in s if x] |
22 |
primes( 100 ) |
一旦你已經設置好了@profile裝飾器,使用kernprof.py執行你的腳步。
1 |
$ kernprof.py - l - 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 x in s 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 |
def primes(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 def primes(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 = n * * 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 x in s if x] |
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 |
x = [ 1 ] |
2 |
y = [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,它看起來是像這樣:
最下面有紅字的盒子是咱們感興趣的對象。咱們能夠看到,它被符號x引用了一次,被列表y引用了三次。若是是x引發了一個內存泄漏,咱們可使用這個方法,經過跟蹤它的全部引用,來檢查爲何它沒有自動的被釋放。
回顧一下,objgraph 使咱們能夠:
在本帖中,我給你顯示了怎樣用幾個工具來分析python程序的性能。經過這些工具與技術的武裝,你能夠得到全部須要的信息,來跟蹤一個python程序中大多數的內存泄漏,以及識別出其速度瓶頸。
對許多其餘觀點來講,運行一次性能分析就意味着在努力目標與事實精度之間作出平衡。若是感到困惑,那麼就實現能適應你目前需求的最簡單的解決方案。