如何使 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