如何使 Python 程式快如閃電?這裡有妙招
- 2020 年 2 月 21 日
- 筆記
所以,讓我們來證明那些人是錯的——讓我們看看如何提高 Python 程式的性能並使它們變得非常快!
時間和性能
在開始優化任何程式碼之前,我們首先需要找出程式碼的哪些部會減慢整個程式的速度。有時,程式的瓶頸可能很明顯,但如果你不知道它在哪裡,那麼你可以從下面幾個地方找到它:
注意:這是我用於演示的程式,它將 e 計算為 X 的冪(取自 Python 文檔):
# slow_program.py from decimal import * def exp(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))
最懶的「剖析」
首先,最簡單、最懶的解決方案——Unix time 命令:
~ $ time python3.8 slow_program.py real 0m11,058s user 0m11,050s sys 0m0,008s
如果你只想給你的整個程式計時,這是可行的,但這通常是不夠的。
最詳細的分析
另一個是 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>) ...
在這裡,我們使用 cProfile 模組和 time 參數運行測試腳本,以便按內部時間(cumtime)對行進行排序。這給了我們很多資訊,你可以看到上面的行大約是實際輸出的 10%。由此我們可以看出 exp 函數是罪魁禍首(是不是感到很驚奇?),現在我們可以更具體地了解時間和分析了!
特定函數計時
既然我們知道該將注意力集中在哪裡,我們可能希望對慢函數進行計時,而不測量程式碼的其餘部分。為此,我們可以使用簡單的 decorator:
def timeit_wrapper(func): @wraps(func) def wrapper(*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)) return func_return_val return wrapper
這個 decorator 隨後可以應用於測試中的函數,如下所示:
@timeit_wrapper def exp(x): ... print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time')) exp(Decimal(150)) exp(Decimal(400)) exp(Decimal(3000))
其輸出如下:
~ $ python3.8 slow_program.py module function time __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 快取的快取/備忘錄
我已經在之前的博文(https://martinheinz.dev/blog/4)中寫過這個,但是我認為有必要用一個簡單的例子來重複一下:
import functools import time # caching up to 12 different results @functools.lru_cache(maxsize=12) def slow_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
上面的函數使用 time.sleep 模擬繁重的計算。當第一次使用參數 1 調用時,它等待 2 秒,然後才返回結果。再次調用時,結果已被快取,因此它跳過函數體並立即返回結果。更多例子,請看之前的博文:https://martinheinz.dev/blog/4 。
使用局部變數
這與在每個範圍內查找變數的速度有關。我編寫每個作用域,因為它不僅僅和局部變數和全局變數的使用有關。實際上,在函數中的局部變數、類級屬性和全局之間的查找速度也存在差異。
你可以通過使用看起來不必要的任務來提高性能,比如:
# Example #1 class FastClass: def do_stuff(self): temp = self.value # this speeds up lookup in loop for i in range(10000): ... # Do something with `temp` here # Example #2 import random def fast_function(): r = random.random for i in range(10000): print(r()) # calling `r()` here, is faster than global random.random()
使用函數
這似乎有悖常理,因為調用函數會把更多的東西放到堆棧中,並從函數返回中產生開銷,但這與前面的觀點有關。如果只將所有的程式碼放在一個文件中而不將其放在函數中,由於全局變數,程式會變慢很多。因此,只需將整個程式碼包裝在 main 函數中並調用一次,就可以加快程式碼的速度,如下所示:
def main(): ... # All your previously global code main()
不訪問屬性
另一個可能會減慢程式速度的是點運算符(.),它在訪問對象屬性時使用。此運算符使用 getattribute 觸發字典查找,這會在程式碼中產生額外的開銷。那麼,我們如何才能真正避免使用它呢?
# Slow: import re def slow_func(): for i in range(10000): re.findall(regex, line) # Slow! # Fast: from re import findall def fast_func(): for i in range(10000): findall(regex, line) # Faster!
小心字元串
在循環中使用例如 module(%s)或 .format()運行時,對字元串的操作可能會非常慢。我們還有什麼更好的選擇?根據 Raymond Hettinger 最近的推文(https://twitter.com/raymondh/status/1205969258800275456),我們唯一應該使用的是 f-string,它是最可讀、最簡潔、最快的方法。因此,根據這條推文,這是你可以使用的方法——從最快到最慢:
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://www.youtube.com/watch?v=OSGv2VnC0go&t=8m17s ),他提到了這些問題。
總結
優化的第一條規則是不要優化。但是,如果你真的需要的話,我希望這幾條建議能幫到你。
via:https://martinheinz.dev/blog/13