python性能分析常用方法

前言:

這篇文章是轉載的!!!
分析一個程序的性能可以歸結爲一下幾個基本的問題:

  • 1.它運行的有多快?
  • 2.它的速度瓶頸在哪裏?
  • 3.它使用了多少內存?
  • 4.哪裏可能發生內訓泄露?

下面提供了幾個看起來很酷的工作,用來解決這幾個問題:

一、使用time模塊粗糙定時:

time是古老的unix工具,可以快速,粗略的測試整個代碼的運行時間(貌似不太適用於服務):

(base) fei-PC:~/Desktop/learn$ time python test_time.py 
start....
end...

real	0m2.010s
user	0m0.009s
sys	0m0.000s

以上三個輸出變量的意義在帖子stackoverflow article 中有詳細介紹。簡單的說:

  • real: 表示實際的程序運行時間;
  • user: 表示程序在用戶態(目態)的cpu總時間;
  • sys: 表示在內核態(管態)的cpu總時間;

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

若sys和user時間之和遠遠少於real時間,那麼你可以猜測你的程序的主要性能問題很可能與IO等待相關。

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

自己實現一個計時上寫文管理器,涉及訪問細粒度即時信息的直接代碼指令。以下是一段代碼,用來做專門的計時測量:
timer.py

import time
"""
構造一個上下文管理器
"""
class Timer(object):
	def __init__(self, verbose=False):
		self.verbose = verbose
	
	def __enter__(self):
		self.start = time.time()
		return self
	
	def __exit__(self, *args, **kwargs):
		self.end = time.time()
		self.secs = self.end - self.start
		self.msecs = self.secs * 1000 # 毫秒值
		if self.verbose:
			print(f"elapsed time:{self.msecs} ms")

爲了使用它,需要在python代碼中用with關鍵字和Timer上下文管理器包裝想要計時的代碼塊. 它將會在代碼塊開始執行的時候啓動計時器,在代碼快結束的時候停止計時.
例子:

from time import Timer

with Timer() as t, open('./test.txt',"r") as fin, open('./out.txt',"w") as fout:
	fout.write(fin.read())
print "=> elasped lpush: %s s" % t.secs

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

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

Robert Kern有一個稱作line_profiler的不錯的項目,我經常使用它查看我的腳步中每行代碼多快多頻繁的被執行。
使用pip安裝:

pip install line_profiler

一旦安裝完成,你將會使用一個稱做“line_profiler”的新模組和一個“kernprof.py”可執行腳本。
想要使用該工具,首先修改你的源代碼,在想要測量的函數上裝飾@profile裝飾器。不要擔心,你不需要導入任何模組。kernprof.py腳本將會在執行的時候將它自動地注入到你的腳步的運行時。
test_profile.py

@profile
def 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]


if __name__ == "__main__":
    primes(100)

設置好@profile裝飾器,使用kernprof.py執行你的腳本。

(base) fei-PC:~/Desktop/learn$ kernprof.py -l -v fib.py

參數:
-l:選項通知kernprof注入@profile裝飾器到你的腳步的內建函數,
-v:選項通知kernprof在腳本執行完畢的時候顯示計時信息。

上述腳本的輸出看起來像這樣:

Wrote profile results to primes.py.lprof
Timer unit: 1e-06 s

File: primes.py
Function: primes at line 2
Total time: 0.00019 s

Line #      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 memory_profiler
pip install psutil
  • 這裏建議安裝psutil包,因爲它可以大大改善memory_profiler的性能

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

@profile
def primes(n): 
    ...
    ...

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

(base) fei-PC:~/Desktop/learn$ python -m memory_profiler primes.py

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

Filename: primes.py

Line #    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 primes
In [2]: %mprun -f primes primes(1000)
In [3]: %lprun -f primes primes(1000)

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

六、內存泄露在哪裏?

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

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

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

  • 使用pip安裝objectgraph:
pip install objgraph

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

import pdb
pdb.set_trace()

執行腳本,進入了一個奇怪的shell功能;
最普遍的對象是哪些?
在運行的時候,你可以通過執行下述指令查看程序中前20個最普遍的對象:

(base) fei-PC:~/Desktop/learn$ python pri.py
> /home/fei/Desktop/learn/pri.py(4)<module>()
-> def primes(n):
(pdb) import objgraph
(pdb) objgraph.show_most_common_types()

MyBigFatObject             20000
tuple                      16938
function                   4310
dict                       2790
wrapper_descriptor         1181
builtin_function_or_method 934
weakref                    764
list                       634
method_descriptor          507
getset_descriptor          451
type                       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() call

traceback                4        +2
KeyboardInterrupt        1        +1
frame                   24        +1
list                   667        +1
tuple                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,它看起來是像這樣:
在這裏插入圖片描述

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

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

  • 顯示佔據python程序內存的頭N個對象
  • 顯示一段時間以後哪些對象被刪除活增加了
  • 在我們的腳本中顯示某個給定對象的所有引用

七、參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章