【編者按】本文作者為 Bryan Helmig,主要介紹 Python 應(yīng)用性能分析的三種進(jìn)階方案颊咬。文章系國內(nèi) ITOM 管理平臺 OneAPM 編譯呈現(xiàn)务甥。
我們應(yīng)該忽略一些微小的效率提升,幾乎在 97% 的情況下喳篇,都是如此:過早的優(yōu)化是萬惡之源敞临。—— Donald Knuth
如果不先想想Knuth的這句名言麸澜,就開始進(jìn)行優(yōu)化工作挺尿,是不明智的。然而炊邦,有時你為了獲得某些特性不假思索就寫下了O(N^2) 這樣的代碼编矾,雖然你很快就忘記它們了,它們卻可能反咬你一口馁害,給你帶來麻煩:本文就是為這種情況而準(zhǔn)備的窄俏。
本文會介紹用于快速分析Python程序的一些有用工具和模式。主要目標(biāo)很簡單:盡快發(fā)現(xiàn)問題碘菜,修復(fù)問題凹蜈,并確認(rèn)修復(fù)是行之有效的限寞。
編寫一個測試
在教程開始前,要先寫一個簡單的概要測試來演示延遲踪区。你可能需要引入一些最小數(shù)據(jù)集來重現(xiàn)可觀的延遲昆烁。通常一或兩秒的運行時間,已經(jīng)足夠在發(fā)現(xiàn)問題時缎岗,讓你做出改進(jìn)了静尼。
此外,進(jìn)行一些基礎(chǔ)測試來確保你的優(yōu)化不會修改緩慢代碼的行為也是有必要的传泊。在分析和調(diào)整時鼠渺,你也可以多次運行這些測試,作為基準(zhǔn)眷细。
那么現(xiàn)在拦盹,我們來看第一個分析工具。
簡單的計時器
計時器是簡單溪椎、靈活的記錄執(zhí)行時間的方法普舆。你可以把它放到任何地方,并且?guī)缀鯖]有副作用校读。自己創(chuàng)建計時器非常簡單沼侣,并且可以根據(jù)你的喜好定制化。例如歉秫,一個簡單的計時器可以這么寫:
import time
def timefunc(f):
def f_timer(*args, **kwargs):
start = time.time()
result = f(*args, **kwargs)
end = time.time()
print f.__name__, 'took', end - start, 'time'
return result
return f_timer
def get_number():
for x in xrange(5000000):
yield x
@timefunc
def expensive_function():
for x in get_number():
i = x ^ x ^ x
return 'some result!'
# prints "expensive_function took 0.72583088875 seconds"
result = expensive_function()
當(dāng)然蛾洛,你可以用上下文管理器來增強它的功能,添加一些檢查點或其他小功能:
import time
class timewith():
def __init__(self, name=''):
self.name = name
self.start = time.time()
@property
def elapsed(self):
return time.time() - self.start
def checkpoint(self, name=''):
print '{timer} {checkpoint} took {elapsed} seconds'.format(
timer=self.name,
checkpoint=name,
elapsed=self.elapsed,
).strip()
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.checkpoint('finished')
pass
def get_number():
for x in xrange(5000000):
yield x
def expensive_function():
for x in get_number():
i = x ^ x ^ x
return 'some result!'
# prints something like:
# fancy thing done with something took 0.582462072372 seconds
# fancy thing done with something else took 1.75355315208 seconds
# fancy thing finished took 1.7535982132 seconds
with timewith('fancy thing') as timer:
expensive_function()
timer.checkpoint('done with something')
expensive_function()
expensive_function()
timer.checkpoint('done with something else')
# or directly
timer = timewith('fancy thing')
expensive_function()
timer.checkpoint('done with something')
有了計時器雁芙,你還需要進(jìn)行一些“挖掘”工作轧膘。 封裝一些更為高級的函數(shù),然后確定問題根源之所在兔甘,進(jìn)而深入可疑的函數(shù)谎碍,不斷重復(fù)。當(dāng)你發(fā)現(xiàn)運行特別緩慢的代碼之后洞焙,修復(fù)它椿浓,然后進(jìn)行測試以確認(rèn)修復(fù)成功。
提示:不要忘了便捷的 timeit 模塊闽晦!將它用于小段代碼塊的基準(zhǔn)校驗比實際測試更加有用扳碍。
- 計時器的優(yōu)點:容易理解和實施,也非常容易在修改前后進(jìn)行對比仙蛉,對于很多語言都適用笋敞。
- 計時器的缺點:有時候,對于非常復(fù)雜的代碼庫而已太過簡單荠瘪,你可能會花更多的時間創(chuàng)建夯巷、替換樣板代碼赛惩,而不是修復(fù)問題!
內(nèi)建分析器
內(nèi)建分析器就好像大型槍械趁餐。雖然非常強大喷兼,但是有點不太好用,有時后雷,解釋和操作起來比較困難季惯。
你可以點此閱讀更多關(guān)于內(nèi)建分析模塊的內(nèi)容,但是內(nèi)建分析器的基本操作非常簡單:你啟用和禁用分析器臀突,它能記錄所有的函數(shù)調(diào)用和執(zhí)行時間勉抓。接著,它能為你編譯和打印輸出候学。一個簡單的分析器用例如下:
import cProfile
def do_cprofile(func):
def profiled_func(*args, **kwargs):
profile = cProfile.Profile()
try:
profile.enable()
result = func(*args, **kwargs)
profile.disable()
return result
finally:
profile.print_stats()
return profiled_func
def get_number():
for x in xrange(5000000):
yield x
@do_cprofile
def expensive_function():
for x in get_number():
i = x ^ x ^ x
return 'some result!'
# perform profiling
result = expensive_function()
在上面代碼中藕筋,控制臺打印的內(nèi)容如下:
5000003 function calls in 1.626 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
5000001 0.571 0.000 0.571 0.000 timers.py:92(get_number)
1 1.055 1.055 1.626 1.626 timers.py:96(expensive_function)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
如你所見,它給出了不同函數(shù)調(diào)用的詳細(xì)數(shù)據(jù)梳码。但是隐圾,它遺漏了一項關(guān)鍵信息:是什么原因,導(dǎo)致函數(shù)運行如此緩慢掰茶?
然而翎承,這對于基礎(chǔ)分析來說是個好的開端。有時符匾,能夠幫你盡快找到解決方案。我經(jīng)常在開始調(diào)試過程時瘩例,把它作為基本測試啊胶,然后再深入測試某個不是運行緩慢,就是調(diào)用頻繁的特定函數(shù)垛贤。
- 內(nèi)建分析器的優(yōu)點:沒有外部依賴焰坪,運行非常快聘惦。對于快速的概要測試非常有用某饰。
- 內(nèi)建分析器的缺點:信息相對有限,需要進(jìn)一步的調(diào)試善绎;報告不太直觀黔漂,尤其是對于復(fù)雜的代碼庫。
Line Profiler
如果內(nèi)建分析器是大型槍械,line profiler就好比是離子炮。它非常的重量級且強大枕磁,使用起來也非常有趣蜕衡。
在這個例子里振坚,我們會用非常棒的kernprof line-profiler噩峦,作為 line_profiler PyPi包笑窜。為了方便使用程储,我們會再次用裝飾器進(jìn)行封裝鳍置,同時也可以防止我們把它留在生產(chǎn)代碼里(因為它比蝸牛還慢)辽剧。
try:
from line_profiler import LineProfiler
def do_profile(follow=[]):
def inner(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 inner
except ImportError:
def do_profile(follow=[]):
"Helpful if you accidentally leave in production!"
def inner(func):
def nothing(*args, **kwargs):
return func(*args, **kwargs)
return nothing
return inner
def get_number():
for x in xrange(5000000):
yield x
@do_profile(follow=[get_number])
def expensive_function():
for x in get_number():
i = x ^ x ^ x
return 'some result!'
result = expensive_function()
如果運行上面的代碼,就會看到以下的報告:
Timer unit: 1e-06 s
File: test.py
Function: get_number at line 43Total time: 4.44195 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
43 def get_number():
44 5000001 2223313 0.4 50.1 for x in xrange(5000000):
45 5000000 2218638 0.4 49.9 yield x
File: test.py
Function: expensive_function at line 47Total time: 16.828 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
47 def expensive_function():
48 5000001 14090530 2.8 83.7 for x in get_number():
49 5000000 2737480 0.5 16.3 i = x ^ x ^ x
50 1 0 0.0 0.0 return 'some result!'
如你所見税产,這是一個非常詳細(xì)的報告怕轿,能讓你完全洞悉代碼的運行情況。和內(nèi)建的cProfiler不同砖第,它能分析核心語言特性的耗時撤卢,比如循環(huán)或?qū)耄⑶医o出不同代碼行的耗時累計值梧兼。
這些細(xì)節(jié)能讓我們更容易理解函數(shù)內(nèi)部原理放吩。 此外,如果需要研究第三方庫羽杰,你可以將其導(dǎo)入渡紫,直接輸?shù)窖b飾器中。
提示:將測試函數(shù)封裝為裝飾器考赛,再將問題函數(shù)作為參數(shù)傳進(jìn)去就好了惕澎!
- Line Profiler 的優(yōu)點:有非常直接和詳細(xì)的報告。能夠追蹤第三方庫里的函數(shù)颜骤。
- Line Profiler 的缺點:因為系統(tǒng)開銷巨大唧喉,會比實際執(zhí)行時間慢一個數(shù)量級,所以不要用它進(jìn)行基準(zhǔn)測試忍抽。同時八孝,它是外部工具。
結(jié)論和最佳方案
你應(yīng)該使用簡單的工具(比如計時器或內(nèi)建分析器)對測試用例(特別是那些你非常熟悉的代碼)進(jìn)行基本檢查鸠项,然后使用更慢但更加細(xì)致的工具干跛,比如 line_profiler
,深入檢查函數(shù)內(nèi)部祟绊。
十有八九楼入,你會發(fā)現(xiàn)一個愚蠢的錯誤,比如在循環(huán)內(nèi)重復(fù)調(diào)用牧抽,或是使用了錯誤的數(shù)據(jù)結(jié)構(gòu)嘉熊,消耗了90%的函數(shù)執(zhí)行時間。在進(jìn)行快速(且令人滿意的)調(diào)整之后扬舒,問題就能得到解決记舆。
如果你仍然覺得程序運行太過緩慢,然后開始進(jìn)行對比屬性訪問(ttribute accessing)方法呼巴,或調(diào)整相等檢查(equality checking)方法等晦澀的調(diào)整泽腮,你可能已經(jīng)適得其反了御蒲。你應(yīng)該考慮如下方法:
1.忍受緩慢或者預(yù)先計算/緩存
2.重新思考整個實施方法
3.使用更多的優(yōu)化數(shù)據(jù)結(jié)構(gòu)(通過 Numpy,Pandas等)
4.編寫一個 C擴展
注意诊赊,優(yōu)化代碼會帶來有罪惡感的快樂厚满!尋找加速Python的合理方法很有趣,但是不要因為加速碧磅,破壞了本身的邏輯碘箍。易讀的代碼比運行速度更重要。實施緩存鲸郊,往往是最簡單的解決方法丰榴。
教程到此為止,希望你今后的Python性能分析能夠如魚得水秆撮!
PS: 點此查看代碼實例四濒。此外,點此學(xué)習(xí)如何如魚得水地調(diào)試 Python 程序职辨。
OneAPM 能幫你查看 Python 應(yīng)用程序的方方面面盗蟆,不僅能夠監(jiān)控終端的用戶體驗,還能監(jiān)控服務(wù)器性能舒裤,同時還支持追蹤數(shù)據(jù)庫喳资、第三方 API 和 Web 服務(wù)器的各種問題。想閱讀更多技術(shù)文章腾供,請訪問 OneAPM 官方技術(shù)博客仆邓。
本文轉(zhuǎn)自 OneAPM 官方博客