最近發現專案API請求比較慢,通過抓包發現主要是response時間太長,於是就開始進行優化工作。優化工作的關鍵一步是定位出問題的瓶頸,對於優化速度來說,從優化函式執行時間這個維度去切入是一個不錯的選擇。
本文側重分析,不展開如何優化
利器
工欲善其事,必先利其器,我們需要一套方便高效的工具記錄函式執行時間。說是一套工具,但對於一個簡單專案或者日常開發來說,實現一個工具類足矣,由於實現比較簡單,直接上程式碼:
from functools import wraps
import cProfile
from line_profiler import LineProfiler
import time
def func_time(f):
"""
簡單記錄執行時間
:param f:
:return:
"""
@wraps(f)
def wrapper(*args, **kwargs):
start = time.time()
result = f(*args, **kwargs)
end = time.time()
print f.__name__, 'took', end - start, 'seconds'
return result
return wrapper
def func_cprofile(f):
"""
內建分析器
"""
@wraps(f)
def wrapper(*args, **kwargs):
profile = cProfile.Profile()
try:
profile.enable()
result = f(*args, **kwargs)
profile.disable()
return result
finally:
profile.print_stats(sort='time')
return wrapper
try:
from line_profiler import LineProfiler
def func_line_time(follow=[]):
"""
每行程式碼執行時間詳細報告
:param follow: 內部呼叫方法
:return:
"""
def decorate(func):
@wraps(func)
def profiled_func(*args, **kwargs):
try:
profiler = LineProfiler()
profiler.add_function(func)
for f in follow:
profiler.add_function(f)
profiler.enable_by_count()
return func(*args, **kwargs)
finally:
profiler.print_stats()
return profiled_func
return decorate
except ImportError:
def func_line_time(follow=[]):
"Helpful if you accidentally leave in production!"
def decorate(func):
@wraps(func)
def nothing(*args, **kwargs):
return func(*args, **kwargs)
return nothing
return decorate
複製程式碼
原始程式碼可以參考gist
如下,實現了3個裝飾器函式func_time
,func_cprofile
,func_line_time
,分別對應
- 簡單輸出函式的執行時間
- 利用python自帶的內建分析包
cProfile
分析,它主要統計函式呼叫以及每個函式所佔的cpu時間 - 利用
line_profiler
開源專案,它可以統計每行程式碼的執行次數和執行時間。
使用說明
我們以一個簡單的迴圈例子來說明一下,
def test():
for x in range(10000000):
print x
複製程式碼
func_time
關於func_time
我覺得沒什麼好說的,就是簡單輸出下函式呼叫時間,這個在我們粗略統計函式執行時間的時候可以使用
如下:
@func_time
def test():
for x in range(10000000):
print x
# 輸出
test took 6.10190296173 seconds
複製程式碼
func_cprofile
cProfile
是python內建包,基於lsprof的用C語言實現的擴充套件應用,執行開銷比較合理,沒有外部依賴,適合做快速的概要測試
@func_cprofile
def test():
for x in range(10000000):
print x
# 輸出
3 function calls in 6.249 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 6.022 6.022 6.249 6.249 test.py:41(test)
1 0.227 0.227 0.227 0.227 {range}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
複製程式碼
輸出說明:
單位為秒
-
第一行告訴我們一共有3個函式被呼叫。
正常開發過程,第一行更多是輸出類似
194 function calls (189 primiive calls) in 0.249 seconds
,(189 primiive calls)表示189個是原生(primitive)呼叫,表明這些呼叫不涉及遞迴 -
ncalls表示函式的呼叫次數,如果這一列有兩個數值,表示有遞迴呼叫,第一個是總呼叫次數,第二個是原生呼叫次數。
-
tottime是函式內部消耗的總時間(不包括呼叫其他函式的時間)。
-
percall是tottime除以ncalls,表示每次呼叫平均消耗時間。
-
cumtime是之前所有子函式消耗時間的累積和。
-
percall是cumtime除以原生呼叫的數量,表示該函式呼叫時,每個原生呼叫的平均消耗時間。
-
filename:lineno(function)為被分析函式所在檔名、行號、函式名。
func_line_time
line_profiler
可以生成非常直接和詳細的報告,但它系統開銷很大,會比實際執行時間慢一個數量級
@func_line_time()
def test():
for x in range(10000000):
print x
# 輸出
Timer unit: 1e-06 s
Total time: 14.4183 s
File: /xx/test.py
Function: test at line 41
Line # Hits Time Per Hit % Time Line Contents
==============================================================
41 @func_line_time()
42 def test():
43 10000001 4031936.0 0.4 28.0 for x in range(10000000):
44 10000000 10386347.0 1.0 72.0 print x
複製程式碼
輸出說明:
單位為微秒
-
Total Time:測試程式碼的總執行時間
-
Line:程式碼行號
-
Hits:表示每行程式碼執行的次數
-
Time:每行程式碼執行的總時間
-
Per Hits:每行程式碼執行一次的時間
-
% Time:每行程式碼執行時間的百分比
總結
日常開發中,可以使用func_time
,func_cprofile
做基本檢查,定位大致問題,使用func_line_time
進行更細緻的深入檢查。
注:
func_line_time
還可以檢查函式內部呼叫的函式執行時間,通過follow
引數指定對應的內部呼叫的函式宣告即可,該引數是個陣列,也就是說可以檢查多個內部呼叫的函式