大數據文摘出品
來源:Medium
編譯:王轉轉
Python已經得到了全球程式設計師的喜愛,但是還是遭到一些人的詬病,原因之一就是認為它運行緩慢。
其實某個特定程序(無論使用何種程式語言)的運行速度是快還是慢,在很大程度上取決於編寫該程序的開發人員自身素質,以及他們編寫優化而高效代碼的能力。
Medium上一位小哥就詳細講了講如何讓python提速30%,以此證明代碼跑得慢不是python的問題,而是代碼本身的問題。
時序分析
在開始進行任何優化之前,我們首先需要找出代碼的哪些部分使整個程序變慢。有時程序的問題很明顯,但是如果你一時不知道問題出在哪裡,那麼這裡有一些可能的選項:
注意:這是我將用於演示的程序,它將進行指數計算(取自Python文檔):
# slow_program.pyfrom decimal import *defexp(x):getcontext().prec +=2i, lasts, s, fact, num =0,0,1,1,1whiles !=lasts:lasts = si +=1fact *= inum *=xs += num / factgetcontext().prec -=2return+sexp(Decimal(150))exp(Decimal(400))exp(Decimal(3000))
最簡約的「配置文件」
首先,最簡單最偷懶的方法——Unix時間命令。
~ $ time python3.8slow_program.pyreal0m11,058suser0m11,050ssys0m0,008s
如果你只能直到整個程序的運行時間,這樣就夠了,但通常這還遠遠不夠。
最詳細的分析
另外一個指令是cProfile,但是它提供的信息過於詳細了。
~ $python3.8-mcProfile -s time slow_program.py1297functioncalls(1272 primitive calls)in11.081secondsOrdered by: internal timencalls tottime percall cumtime percall filename:lineno(function)311.0793.69311.0793.693slow_program.py:4(exp)10.0000.0000.0020.002{built-in method _imp.create_dynamic}4/10.0000.00011.08111.081{built-in method builtins.exec}60.0000.0000.0000.000{built-in method __new__ oftypeobject at0x9d12c0}60.0000.0000.0000.000abc.py:132(__new__)230.0000.0000.0000.000_weakrefset.py:36(__init__)2450.0000.0000.0000.000{built-in method builtins.getattr}20.0000.0000.0000.000{built-in method marshal.loads}100.0000.0000.0000.000<frozen importlib._bootstrap_external>:1233(find_spec)8/40.0000.0000.0000.000abc.py:196(__subclasscheck__)150.0000.0000.0000.000{built-in method posix.stat}60.0000.0000.0000.000{built-in method builtins.__build_class__}10.0000.0000.0000.000__init__.py:357(namedtuple)480.0000.0000.0000.000<frozen importlib._bootstrap_external>:57(_path_join)480.0000.0000.0000.000<frozen importlib._bootstrap_external>:59(<listcomp>)10.0000.00011.08111.081slow_program.py:1(<module>)
在這裡,我們使用cProfile模塊和time參數運行測試腳本,以便按內部時間(cumtime)對行進行排序。這給了我們很多信息,你在上面看到的行大約是實際輸出的10%。由此可見,exp函數是罪魁禍首,現在我們可以更詳細地了解時序和性能分析。
時序特定功能
現在我們知道了應當主要關注哪裡,我們可能想對運行速度緩慢的函數計時,而不用測量其餘的代碼。為此,我們可以使用一個簡單的裝飾器:
deftimeit_wrapper(func):@wraps(func)defwrapper(*args, **kwargs):start = time.perf_counter()# Alternatively, you can use time.process_time()func_return_val = func(*args, **kwargs)end= time.perf_counter()print('{0:<10}.{1:<8} : {2:<8}'.format(func.__module__, func.__name__,end- start))returnfunc_return_valreturnwrapper
然後可以將此裝飾器應用於待測功能,如下所示:
@timeit_wrapperdefexp(x):...print('{0:<10} {1:<8} {2:^8}'.format('module','function','time'))exp(Decimal(150))exp(Decimal(400))exp(Decimal(3000))
這給出我們如下輸出:
~ $python3.8slow_program.pymodulefunctiontime__main__ .exp:0.003267502994276583__main__ .exp:0.038535295985639095__main__ .exp:11.728486061969306
需要考慮的一件事是我們實際想要測量的時間。時間包提供time.perf_counter和time.process_time兩個函數。他們的區別在於perf_counter返回的絕對值,包括你的Python程序進程未運行時的時間,因此它可能會受到計算機負載的影響。另一方面,process_time僅返回用戶時間(不包括系統時間),這僅是你的過程時間。
加速吧!
讓Python程序運行得更快,這部分會很有趣!我不會展示可以解決你的性能問題的技巧和代碼,更多地是關於構想和策略的,這些構想和策略在使用時可能會對性能產生巨大影響,在某些情況下,可以將速度提高30%。
使用內置數據類型
這一點很明顯。內置數據類型非常快,尤其是與我們的自定義類型(例如樹或連結列表)相比。這主要是因為內置程序是用C實現的,因此在使用Python進行編碼時我們的速度實在無法與之匹敵。
使用lru_cache緩存/記憶
我已經在上一篇博客中展示了此內容,但我認為值得用簡單的示例來重複它:
importfunctoolsimporttime# caching up to 12 different results@functools.lru_cache(maxsize=12)defslow_func(x):time.sleep(2)# Simulate long computationreturnxslow_func(1)# ... waiting for 2 sec before getting resultslow_func(1)# already cached - result returned instantaneously!slow_func(3)# ... waiting for 2 sec before getting result
上面的函數使用time.sleep模擬大量計算。第一次使用參數1調用時,它將等待2秒鐘,然後才返回結果。再次調用時,結果已經被緩存,因此它將跳過函數的主體並立即返回結果。有關更多實際示例,請參見以前的博客文章。
使用局部變量
這與在每個作用域中查找變量的速度有關,因為它不只是使用局部變量還是全局變量。實際上,即使在函數的局部變量(最快),類級屬性(例如self.name——較慢)和全局(例如,導入的函數)如time.time(最慢)之間,查找速度實際上也有所不同。
你可以通過使用看似不必要的分配來提高性能,如下所示:
# Example #1classFastClass:defdo_stuff(self):temp = self.value# this speeds up lookup in loopforiinrange(10000):...# Do something with `temp` here# Example #2importrandomdeffast_function():r = random.randomforiinrange(10000):print(r())# calling `r()` here, is faster than global random.random()
使用函數
這似乎違反直覺,因為調用函數會將更多的東西放到堆棧上,並從函數返回中產生開銷,但這與上一點有關。如果僅將整個代碼放在一個文件中而不將其放入函數中,則由於全局變量,它的運行速度會慢得多。因此,你可以通過將整個代碼包裝在main函數中並調用一次來加速代碼,如下所示:
defmain():...# All your previously global codemain()
不訪問屬性
可能會使你的程序變慢的另一件事是點運算符(.),它在獲得對象屬性時被使用。此運算符使用__getattribute__觸發字典查找,這會在代碼中產生額外的開銷。那麼,我們如何才能真正避免(限制)使用它呢?
# Slow:importredefslow_func():foriinrange(10000):re.findall(regex, line)# Slow!# Fast:fromreimportfindalldeffast_func():foriinrange(10000):findall(regex, line)# Faster!
當心字符串
使用模數(%s)或.format()進行循環運行時,字符串操作可能會變得非常慢。我們有什麼更好的選擇?根據雷蒙德·海廷格(Raymond Hettinger)最近的推特,我們唯一應該使用的是f字符串,它是最易讀,最簡潔且最快的方法。根據該推特,這是你可以使用的方法列表——最快到最慢:
f'{s}{t}'# Fast!s +' '+ t' '.join((s, t))'%s %s'% (s, t)'{} {}'.format(s, t)Template('$s $t').substitute(s=s, t=t)# Slow!
生成器本質上並沒有更快,因為它們被允許進行延遲計算,從而節省了內存而不是時間。但是,保存的內存可能會導致你的程序實際運行得更快。這是怎麼做到的?如果你有一個很大的數據集,而沒有使用生成器(迭代器),那麼數據可能會溢出CPU L1緩存,這將大大減慢內存中值的查找速度。
在性能方面,非常重要的一點是CPU可以將正在處理的所有數據儘可能地保存在緩存中。你可以觀看Raymond Hettingers的視頻,他在其中提到了這些問題。
結論
優化的首要規則是不要優化。但是,如果確實需要,那麼我希望上面這些技巧可以幫助你。但是,在優化代碼時要小心,因為它可能最終使你的代碼難以閱讀,因此難以維護,這可能超過優化的好處。
相關報導:
https://towardsdatascience.com/making-python-programs-blazingly-fast-c1cd79bd1b32