教你幾招,Python性能提升30%!
本文主要分享如何提升Python性能的幾個使用方法!
時序分析
優(yōu)化之前,首先要找到是哪部分代碼拖慢了整個程序的運行。有時候程序的"瓶頸"不是很明顯,如果找不到,以下是一些建議以供參考:
注意:這是一個計算e的x次冪的演示程序(出自Python文檔):
- # slow_program.py
- from decimal import*
- defexp(x):
- getcontext().prec +=2
- i, lasts, s, fact, num =0, 0, 1, 1, 1
- while s != lasts:
- lasts = s
- i +=1
- fact *= i
- num *= x
- s += num / fact
- getcontext().prec -=2
- return+s
- exp(Decimal(150))
- exp(Decimal(400))
- exp(Decimal(3000))
在GitHub上查看rawslow_program.py全部代碼
最省力的“性能分析”
首先,最簡單且最省力的解決方案是使用Unix的time命令:
- ~ $ time python3.8 slow_program.py
- real 0m11,058s
- user 0m11,050s
- sys 0m0,008s
在GitHub上查看rawbase_time.shell全部代碼
如果只是給整個程序計時,它很有用,但還不足夠……
最詳細的性能分析
性能分析的另一方法是cProfile,從中能得到很大的信息量:
- ~ $ python3.8 -m cProfile -s time slow_program.py
- 1297 function calls (1272 primitive calls) in 11.081 seconds
- Ordered by: internal time
- ncalls tottime percall cumtime percall filename:lineno(function)
- 3 11.079 3.693 11.079 3.693 slow_program.py:4(exp)
- 1 0.000 0.000 0.002 0.002 {built-in method _imp.create_dynamic}
- 4/1 0.000 0.000 11.081 11.081 {built-in method builtins.exec}
- 6 0.000 0.000 0.000 0.000 {built-in method __new__ of type object at 0x9d12c0}
- 6 0.000 0.000 0.000 0.000 abc.py:132(__new__)
- 23 0.000 0.000 0.000 0.000 _weakrefset.py:36(__init__)
- 245 0.000 0.000 0.000 0.000 {built-in method builtins.getattr}
- 2 0.000 0.000 0.000 0.000 {built-in method marshal.loads}
- 10 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:1233(find_spec)
- 8/4 0.000 0.000 0.000 0.000 abc.py:196(__subclasscheck__)
- 15 0.000 0.000 0.000 0.000 {built-in method posix.stat}
- 6 0.000 0.000 0.000 0.000 {built-in method builtins.__build_class__}
- 1 0.000 0.000 0.000 0.000 __init__.py:357(namedtuple)
- 48 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:57(_path_join)
- 48 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:59(<listcomp>)
- 1 0.000 0.000 11.081 11.081 slow_program.py:1(<module>)
- ...
在GitHub上查看rawcprofile.shell全部代碼
這里用cProfile模塊和time參數(shù)運行測試腳本,以便按內(nèi)部時間(cumtime)對行進行排序。從中可以得到很多信息,以上所列結(jié)果約為實際輸出的10%。由此可見,exp函數(shù)就是拖慢程序的“罪魁禍首”(太神奇啦?。?,現(xiàn)在看看更詳盡的時序和性能分析......
對特定函數(shù)計時
已經(jīng)知道拖慢程序運行的函數(shù),下一步可使用簡單的修飾器,專門對該函數(shù)計時,不測量其余代碼。如下所示:
- deftimeit_wrapper(func):
- @wraps(func)
- defwrapper(*args, **kwargs):
- start = time.perf_counter() # Alternatively, you can use time.process_time()
- funcfunc_return_val = func(*args, **kwargs)
- end = time.perf_counter()
- print( {0:<10}.{1:<8} : {2:<8} .format(func.__module__, func.__name__, end - start))
- return func_return_val
- return wrapper
在GitHub上查看rawtimeit_decorator.py全部代碼
該修飾器可以應(yīng)用于功能測試,如下所示:
- @timeit_wrapper
- defexp(x):
- ...
- print( {0:<10}{1:<8}{2:^8} .format( module , function , time ))
- exp(Decimal(150))
- exp(Decimal(400))
- exp(Decimal(3000))
在GitHub上查看rawtimeit_decorator_usage.py全部代碼
輸出如下:
- ~ $ python3.8 slow_program.py
- module function time
- __main__ .exp :0.003267502994276583
- __main__ .exp :0.038535295985639095
- __main__ .exp : 11.728486061969306
在GitHub上查看rawrun_with_timeit_decorator.shell全部代碼
要考慮的一個問題是實際/想要測量的時間類型是什么。Time程序包提供了time.perf_counter和time.process_time。兩者的區(qū)別是:perf_counter返回絕對值,其中包括Python程序進程未運行時的時間,因此可能會受計算機負載的影響;而process_time僅返回用戶時間(不包括系統(tǒng)時間),這僅是程序的運行時間。
加快程序運行速度
圖源:Unsplash
這是全文有趣的部分,關(guān)于如何加快Python的程序運行速度。我并沒有列出一些可以奇妙解決性能問題的小技巧或代碼段,而是涉及一般性的構(gòu)想和策略,它們能極大地提高性能,某些情況下甚至能將性能提高30%。
使用內(nèi)置數(shù)據(jù)類型
顯而易見,內(nèi)置數(shù)據(jù)類型運行很快,尤其是與自定義類型(例如樹或鏈表)相比。主要是因為內(nèi)置程序是用C語言實現(xiàn)的,遠超過用Python編碼的運行速度。
使用lru_cache緩存/記憶
我已經(jīng)在上一篇博文中講過這塊內(nèi)容,但在此還是要用簡單的示例說明:
- import functools
- import time
- # caching up to 12 different results
- @functools.lru_cache(maxsize=12)
- defslow_func(x):
- time.sleep(2) # Simulate long computation
- return x
- slow_func(1) # ... waiting for 2 sec before getting result
- slow_func(1) # already cached - result returned instantaneously!
- slow_func(3) # ... waiting for 2 sec before getting result
在GitHub上查看rawlru_cache.py全部代碼
以上函數(shù)使用time.sleep模擬大量運算。第一次使用參數(shù)1調(diào)用該函數(shù)時,返回結(jié)果需要2秒。再次調(diào)用時,結(jié)果已被緩存,因此會跳過函數(shù)主體并立即返回結(jié)果。更多內(nèi)容請參見此處。
使用局部變量
這與在每個作用域中查找變量的速度有關(guān)。我用了“每個作用域”這個字眼,因為它不僅僅是“使用局部變量還是全局變量”的問題。實際上,即使在函數(shù)的局部變量(最快)、類級屬性(如self.name-較慢)和全局變量(如導入的函數(shù),time.time-最慢)之間,查找速度也有所不同。
可以通過運行無用的任務(wù)來提高性能,如下所示:
- # Example #1
- classFastClass:
- defdo_stuff(self):
- temp =self.value # this speeds up lookup in loop
- for i inrange(10000):
- ... # Do something with `temp` here
- # Example #2
- import random
- deffast_function():
- r = random.random
- for i inrange(10000):
- print(r()) # calling `r()` here, is faster than global random.random()
在GitHub上查看rawlocal_vars.py全部代碼
使用函數(shù)(Function)
這怎么和假想的不同?理論上調(diào)用函數(shù)不是會將更多的東西放到堆棧上,加大返回結(jié)果的負擔嗎?但實際上,使用函數(shù)確實能加快運行速度,這與前一點有關(guān)。將整個代碼放在一個文件中而非函數(shù)中,它是全局變量而非局部變量,運行速度就會慢得多。因此,可以將整個代碼包裹在main函數(shù)中并通過一次調(diào)用來加速代碼,如下所示:
- defmain():
- ... # All your previously global code
- main()
在GitHub上查看rawglobal_vars.py全部代碼
避免訪問屬性(Attribute)
可能拖慢程序的一個原因是使用點運算符(.)訪問對象屬性。該運算符通過使用__getattribute__方法觸發(fā)了字典查找,使代碼產(chǎn)生額外負擔。那么,如何避免或減少屬性訪問?
- # Slow:
- import re
- defslow_func():
- for i inrange(10000):
- re.findall(regex, line) # Slow!
- # Fast:
- from re import findall
- deffast_func():
- for i inrange(10000):
- findall(regex, line) # Faster!
在GitHub上查看rawimports.py全部代碼
當心使用字符串
在循環(huán)里使用格式符(%s)或.format()時,字符串操作可能會變得非常慢。有沒有更好的選擇?Raymond Hettinger在最近發(fā)布的推文中提到:唯一應(yīng)該使用的是f-string(格式化字符串常量),它是最易讀、最簡潔且最快捷的方法。根據(jù)這篇推文,下面列出了可用的方法(由快到慢):
- f {s}{t} # Fast!
- s + + t
- .join((s, t))
- %s %s % (s, t)
- {} {} .format(s, t)
- Template( $s $t ).substitute(ss=s, tt=t) # Slow!
在GitHub上查看rawstrings.py全部代碼
本質(zhì)上,生成器并沒有變得更快,因為它在設(shè)計上允許延遲計算以節(jié)省內(nèi)存而非節(jié)約時間。然而節(jié)省的內(nèi)存也可以加快程序?qū)嶋H運行速度。怎么做?如果有一個很大的數(shù)據(jù)集且不使用生成器(迭代器),那么數(shù)據(jù)可能會溢出CPU的L1 cache(1級緩存),這將大大減慢內(nèi)存的查找速度。
在性能方面,極重要的一點是:CPU可以將正在處理的所有數(shù)據(jù)盡可能地保存在緩存中。
圖源:Unsplash
結(jié)語
優(yōu)化的首要規(guī)則就是“不優(yōu)化”。
若真的有必要優(yōu)化,那我希望這些技巧會有所幫助。
但是,優(yōu)化代碼時一定要小心,因為優(yōu)化的結(jié)果可能是代碼難以閱讀進而難以維護,這就得不償失了。
最后,希望大家能搭上python號火箭,編碼越來越快!