1. 程式人生 > >Python效能分析優化以及測試

Python效能分析優化以及測試

寫在前面

分析工作的一個好的方式是在分析之前對程式碼的各部分的執行速度和記憶體消耗情況做一個假設或者預判,然後結合分析工具的分析結果驗證或者糾正假設,不斷提高對程式碼的分析能力。

CPU分析工具

簡單的time方法以及包裝成的修飾器

Python效能分析最簡單的方法是通過內建time模組的測量,然後打印出來,但是這種方法只適用於簡短的調查,用的過多會比較雜亂。如下:

t1= time.time()
result = fn(*args, **kwargs)
t2 = time.time()
print('@timefn:' + fn.func_time + 'took'
+ str(t2-t1) + 'second')

比較乾淨的方法是使用修飾器,在被需要調查的函式定義上面增加一行修飾語句。使用這個修飾器的開銷很小,但是成千上萬次會變得引人注意。

from functools import wraps

def timefn(fn):
    @wraps(fn)
    def measure_time(*args, **kwargs): # *arg數量可變的位置引數,**kwargs數量可變的鍵值對引數
        t1= time.time()
        result = fn(*args, **kwargs)
        t2 =
time.time() print('@timefn:' + fn.func_time + 'took' + str(t2-t1) + 'second') return result return measure_time

這裡定義了一個新函式timefn,它一個函式fn為引數,使用@wraps(fn)將函式名和docstring暴露給fn的呼叫者。

timeit

timeit一般用來測量簡單語句的執行速度,作為一種輔助分析手段。timeit模組暫時禁用了垃圾收集器,如果測試程式碼中呼叫了垃圾收集器會影響到執行速度。方法:

python -
m timeit -n 5 -r 5 -s "import modulename" "function(arg1 = **, arg2 = **)"

‘ -s’用來匯入被測函式的模組;‘-n’指定迴圈次數,取平均數;‘-r’為重複次數,選平均數中的最好結果。這種方法與IPython中‘%time’方法類似。

UNIX的time命令進行簡單的計時

使用UNIX作業系統的標準系統功能記錄程式執行所耗費的各方面時間,且不在意程式碼的內部結構。使用方法:

/usr/bin/time -p python name.py

輸出結果分三個部分:real記錄了整體的耗時, user記錄了CPU花在任務上的時間,但不包括核心函式耗費的時間, sys記錄了核心函式耗費的時間。user和sys相加的和就得到CPU花費的總時間,而這個時間和real的差可能就是花費在等待IO上,也可能是系統正忙著執行其他任務因此影響了測量。另外比較有用的方法是用‘–verbose’來獲得更多輸出資訊。

/usr/bin/time -verbose python name.py

列印的資訊中很有用的一項是‘Major(requiring I/O) page faults’,因為它指示了記憶體缺頁引發的效能懲罰。

cProfile:分析每個函式執行花費的時間

cProfile是一個標準庫內建工具,通過鉤入CPython虛擬機器來測量每一個函式所花費的時間,這一技術會引入額外的巨大開銷。執行如下命令,cProfile會將輸出直接列印到螢幕。

python -m cProfile -s cumulative name.py

‘-s’的意義是設定cProfile的分析結果對每個函式累計花費的時間進行排序。然後還可以生成統計檔案,然後通過Python進行分析。

python - m cProfile - o profile.stats name.py
import pstats
p = pstats.Stats("profile.stats")
p.sort_stats("cumulative")
p.print_stats()

另外,我們還可以通過‘print_callers()’和‘print_callees()’方法打印出呼叫者和被呼叫者的資訊。

p.print_callers()
p.print_callees()

line_profiler:逐行分析

line_profile可以對函式進行組行分析,應該先用cProfile找到最需要分析的函式,然後用line_profile對函式進行分析,通過pip安裝。

pip install line_profiler

使用方法:先通過修飾器‘@profile’標記需要分析的函式,然後執行如下命令。

kernprof -l -v name.py

'-l’代表組行分析而不是逐個函式分析, ‘-v’代表顯示輸出,分析過程會產生一個字尾為’.lprof’的檔案。得到結果觀察哪條語句執行佔用的時間最多,然後進行進一步分析,嘗試更換執行順序、調整演算法、編譯和型別指定等方法來優化,並用後續的測試進行驗證。可以運用‘%timeit’來對比測量一些語句的效率;Python語句的評估次序是從左至右且支援短路,可以將執行開銷大的語句放在右邊。

記憶體分析工具

memory_profiler: 記憶體用量診斷工具

memory_profile包最好有psutil包的支援,二者皆可以通過pip安裝。

pip install psutil memory_profiler

memory_profiler的對效能的影響很大(10-100倍的降速,比line_profiler的效率還低),所以最好將測試侷限在一個較小的問題上,或者在程式碼成長的早期進行分析。使用的方法分兩步,第一步是在原始碼中用‘@profile’對需要分析的函式進行標記,第二步是呼叫memory_profiler模組進行分析。

python -m memory_profiler code_need_analyse.py

IPython中可以使用‘%memit’魔法函式來測量某些語句的RAM使用情況,工作方式類似於‘%timeit’
特別說明:
在處理記憶體分配時,情況並不像CPU佔有率那麼直截了當,通常一個程序將記憶體超額分配給本地記憶體池並在空閒時使用會更有效率,因為一次記憶體分配的操作非常昂貴;
另外,垃圾收集不會立即執行,所以物件被銷燬後依然會存在垃圾收集池中一段時間;
這些技術的後或是很難真正瞭解一個Python程式內部的記憶體使用和釋放情況,因為當從程序外部觀察時,某一段程式碼可能不會分配固定數量的記憶體,觀察多行程式碼的記憶體佔有趨勢可能比只觀察一行程式碼更有參考價值。

heapy:記憶體堆調查工具

藉助heapy可以深入直譯器內部檢視記憶體堆中有多少物件被使用以及它們是否被垃圾收集時,是解決記憶體洩漏(可能由於某個物件的引用隱藏於一個複雜系統中)的有效工具;還可以通過審查去考察是否如預期的那樣生成物件。使用heapy需要通過pip安裝guppy包。

pip install guppy

使用方法:

def calc_pure_python(draw_output, desired_width, max_iterations):
    ...
    while xcoord < x2:
        x.append(xcoord)
        xcoord += x_step
    from guppy import hpy; hp = hpy()
    print("heapy after creating y and x lists of floats")
    h = hp.heap()
    print(h) 
    print
    zs = []
    cs = []
    for ycoord in y:
        for xcoord in x:
            zs.append(complex(xcoord, ycoord))
            zs.append(complex(c_real, c_imag))
    print("heapy after creating zs and cs using complex numbers")
    h = hp.heap()
    print(h)

用dis模組檢查CPython位元組碼

dis模組是Pyhon內建的包,通過傳給它一段程式碼或者一個模組可以檢視到基於棧的CPython虛擬機器執行的位元組碼,增強對Python程式碼模型的理解,可以瞭解到為什麼某些編碼風格會比其他的更快,同時還能幫助使用Cython這樣的工具,他跳出了Python的範疇,能夠生成C程式碼。使用方法:

import dis
dis.dis(module or function name)