效能分析和調優工具簡介
總會遇到一個時候你會想提高程式執行效率,想看看哪部分耗時長成為瓶頸,想知道程式執行時記憶體和CPU使用情況。這時候你會需要一些方法對程式進行效能分析和調優。
By Context Manager
可以上下文管理器自己實現一個計時器, 參見之前的介紹 timeit
文章裡做的那樣,通過定義類的 __enter__
和 __exit__
方法來實現對管理的函式計時, 類似如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# 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): self.end = time.time() self.secs = self.end - self.start self.msecs = self.secs * 1000 # 毫秒 if self.verbose: print 'elapsed time: %f ms' % self.msecs |
使用方式如下:
1 2 3 4 5 6 |
from timer import Timer with Timer() as t: foo() print "=> foo() spends %s s" % t.secs |
By Decorator
然而我認為裝飾器的方式更加優雅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import time from functools import wraps def timer(function): @wraps(function) def function_timer(*args, **kwargs): t0 = time.time() result = function(*args, **kwargs) t1 = time.time() print ("Total time running %s: %s seconds" % (function.func_name, str(t1-t0)) ) return result return function_timer |
使用就很簡單了:
1 2 3 4 5 6 7 |
<a href="http://www.jobbole.com/members/Timer">@timer</a> def my_sum(n): return sum([i for i in range(n)]) if __name__ == "__main__": my_sum(10000000) |
執行結果:
1 2 3 |
➜ python profile.py Total time running my_sum: 0.817697048187 seconds |
系統自帶的time命令
使用示例如下:
1 2 3 4 |
➜ time python profile.py Total time running my_sum: 0.854454040527 seconds python profile.py 0.79s user 0.18s system 98% cpu 0.977 total |
上面的結果說明: 執行指令碼消耗0.79sCPU時間, 0.18秒執行核心函式消耗的時間,總共0.977s時間。
其中, total時間 - (user時間 + system時間) = 消耗在輸入輸出和系統執行其它任務消耗的時間
python timeit 模組
可以用來做benchmark, 可以方便的重複一個程式執行的次數,來檢視程式可以執行多塊。具體參考之前寫的文章。
cProfile
直接看帶註釋的使用示例吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#coding=utf8 def sum_num(max_num): total = 0 for i in range(max_num): total += i return total def test(): total = 0 for i in range(40000): total += i t1 = sum_num(100000) t2 = sum_num(200000) t3 = sum_num(300000) t4 = sum_num(400000) t5 = sum_num(500000) test2() return total def test2(): total = 0 for i in range(40000): total += i t6 = sum_num(600000) t7 = sum_num(700000) return total if __name__ == "__main__": import cProfile # # 直接把分析結果列印到控制檯 # cProfile.run("test()") # # 把分析結果儲存到檔案中 # cProfile.run("test()", filename="result.out") # 增加排序方式 cProfile.run("test()", filename="result.out", sort="cumulative") |
cProfile將分析的結果儲存到result.out檔案中,但是以二進位制形式儲存的,想直接檢視的話用提供的 pstats
來檢視。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import pstats # 建立Stats物件 p = pstats.Stats("result.out") # strip_dirs(): 去掉無關的路徑資訊 # sort_stats(): 排序,支援的方式和上述的一致 # print_stats(): 列印分析結果,可以指定列印前幾行 # 和直接執行cProfile.run("test()")的結果是一樣的 p.strip_dirs().sort_stats(-1).print_stats() # 按照函式名排序,只列印前3行函式的資訊, 引數還可為小數,表示前百分之幾的函式資訊 p.strip_dirs().sort_stats("name").print_stats(3) # 按照執行時間和函式名進行排序 p.strip_dirs().sort_stats("cumulative", "name").print_stats(0.5) # 如果想知道有哪些函式呼叫了sum_num p.print_callers(0.5, "sum_num") # 檢視test()函式中呼叫了哪些函式 p.print_callees("test") |
擷取一個檢視test()呼叫了哪些函式的輸出示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
➜ python python profile.py Random listing order was used List reduced from 6 to 2 due to restriction Function called... ncalls tottime cumtime profile.py:24(test2) -> 2 0.061 0.077 profile.py:3(sum_num) 1 0.000 0.000 {range} profile.py:10(test) -> 5 0.073 0.094 profile.py:3(sum_num) 1 0.002 0.079 profile.py:24(test2) 1 0.001 0.001 {range} |
profile.Profile
cProfile還提供了可以自定義的類,可以更精細的分析, 具體看文件。
格式如: class profile.Profile(timer=None, timeunit=0.0, subcalls=True, builtins=True)
下面這個例子來自官方文件:
1 2 3 4 5 6 7 8 9 10 11 |
import cProfile, pstats, StringIO pr = cProfile.Profile() pr.enable() # ... do something ... pr.disable() s = StringIO.StringIO() sortby = 'cumulative' ps = pstats.Stats(pr, stream=s).sort_stats(sortby) ps.print_stats() print s.getvalue() |
lineprofiler
lineprofiler是一個對函式進行逐行效能分析的工具,可以參見github專案說明,地址: https://github.com/rkern/line…
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#coding=utf8 def sum_num(max_num): total = 0 for i in range(max_num): total += i return total @profile # 新增@profile 來標註分析哪個函式 def test(): total = 0 for i in range(40000): total += i t1 = sum_num(10000000) t2 = sum_num(200000) t3 = sum_num(300000) t4 = sum_num(400000) t5 = sum_num(500000) test2() return total def test2(): total = 0 for i in range(40000): total += i t6 = sum_num(600000) t7 = sum_num(700000) return total test() |
通過 kernprof
命令來注入分析,執行結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
➜ kernprof -l -v profile.py Wrote profile results to profile.py.lprof Timer unit: 1e-06 s Total time: 3.80125 s File: profile.py Function: test at line 10 Line # Hits Time Per Hit % Time Line Contents ============================================================== 10 @profile 11 def test(): 12 1 5 5.0 0.0 total = 0 13 40001 19511 0.5 0.5 for i in range(40000): 14 40000 19066 0.5 0.5 total += i 15 16 1 2974373 2974373.0 78.2 t1 = sum_num(10000000) 17 1 58702 58702.0 1.5 t2 = sum_num(200000) 18 1 81170 81170.0 2.1 t3 = sum_num(300000) 19 1 114901 114901.0 3.0 t4 = sum_num(400000) 20 1 155261 155261.0 4.1 t5 = sum_num(500000) 21 1 378257 378257.0 10.0 test2() 22 23 1 2 2.0 0.0 return total |
hits(執行次數) 和 time(耗時) 值高的地方是有比較大優化空間的地方。
memoryprofiler
類似於”lineprofiler“對基於行分析程式記憶體使用情況的模組。github 地址:https://github.com/fabianp/me… 。ps:安裝 psutil
, 會分析的更快。
同樣是上面”lineprofiler“中的程式碼,執行 python -m memory_profiler profile.py
命令生成結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
➜ python -m memory_profiler profile.py Filename: profile.py Line # Mem usage Increment Line Contents ================================================ 10 24.473 MiB 0.000 MiB @profile 11 def test(): 12 24.473 MiB 0.000 MiB total = 0 13 25.719 MiB 1.246 MiB for i in range(40000): 14 25.719 MiB 0.000 MiB total += i 15 16 335.594 MiB 309.875 MiB t1 = sum_num(10000000) 17 337.121 MiB 1.527 MiB t2 = sum_num(200000) 18 339.410 MiB 2.289 MiB t3 = sum_num(300000) 19 342.465 MiB 3.055 MiB t4 = sum_num(400000) 20 346.281 MiB 3.816 MiB t5 = sum_num(500000) 21 356.203 MiB 9.922 MiB test2() 22 23 356.203 MiB 0.000 MiB return total |