开发速度极快的Python程序

讨厌Python的人总是说,他们不想使用这种语言的原因之一是因为Python速度慢。 但是,无论使用哪种编程语言,都可以将某个程序视为快速还是慢速的事实,在很大程度上取决于编写该程序的开发人员,其知识以及创建优化的高性能代码的能力。



我们今天要发表的这篇文章的作者提出,证明那些称Python慢​​的人是错误的。 他想谈谈如何提高Python程序的性能并使它们真正快速。

时间测量和分析


在开始优化任何代码之前,首先需要找出其中的哪些部分会使整个程序变慢。 有时程序瓶颈可能很明显,但是如果程序员不知道它在哪里,他可以利用一些机会来识别它。

下面是该程序的代码,我将用于演示目的。 它取自Python文档。 此代码将e提升为x的幂:

 # 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工具, 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 )排序。 它给了我们很多信息。 实际上,上面显示的只是cProfile输出的大约10%。

在分析了这些数据之后,我们可以看到exp函数是程序运行缓慢的原因(这很奇怪!)。 之后,我们可以使用更精确的工具进行代码分析。

研究特定职能的临时绩效指标


现在,我们知道了需要引起我们注意的程序位置。 因此,我们可能决定在不分析其他程序代码的情况下研究慢速功能。 为此,您可以使用一个简单的装饰器:

 def timeit_wrapper(func):    @wraps(func)    def wrapper(*args, **kwargs):        start = time.perf_counter() #       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 

该装饰器可以应用于要探索的功能:

 @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_countertime.perf_counter指示符。 它们之间的区别在于perf_counter返回一个绝对值,其中包括Python程序进程未运行的时间。 这意味着该指示器可能会受到其他程序在计算机上造成的负载的影响。 process_time仅返回用户时间。 它不包括系统时间。 这仅给我们有关流程执行时间的信息。

代码加速


现在是有趣的部分。 让我们继续加速程序。 我(大部分情况下)不会在这里展示各种可以神奇地解决性能问题的技巧,技巧和神秘代码。 我基本上想谈论一些通用的想法和策略,如果使用它们,它们会对性能产生重大影响。 在某些情况下,我们谈论的是代码执行速度提高30%。

▍使用内置数据类型


使用内置数据类型是加速代码的非常明显的方法。 内置数据类型非常快,尤其是将它们与自定义类型(如树或链表)进行比较时。 这里的重点主要是使用C实现语言的内置机制。如果使用Python描述某些内容,则无法达到相同的性能水平。

with使用lru_cache应用缓存(记忆)


缓存是一种提高代码性能的流行方法。 我已经写过关于他的文章,但我认为在这里值得一提:

 import functools import time #   12   @functools.lru_cache(maxsize=12) def slow_func(x):    time.sleep(2) #       return x slow_func(1) # ...  2     slow_func(1) #    -   ! slow_func(3) # ...   2     

上面的函数使用time.sleep模拟复杂的计算。 第一次使用参数1调用它时,它将等待2秒钟,然后才返回结果。 当再次使用相同的参数调用她时,结果表明她的工作结果已被缓存。 在这种情况下,函数的主体不执行,结果将立即返回。 在这里,您可以找到更接近实际的缓存示例。

▍使用局部变量


应用局部变量,我们考虑了在每个范围内搜索变量的速度。 我在特别谈论“可见性的每个领域”,因为在这里我不仅考虑将工作速度与局部变量和全局变量进行比较。 实际上,甚至可以观察到使用变量的区别,例如,在函数中的局部变量(速度最高),类级别的属性(例如self.name ,这已经很慢)和全局导入的实体(例如time.time (最大)。这三种机制的速度较慢)。

您可以使用以下方法来提高性能,以为那些不知情的人分配似乎完全不必要和无用的值:

 #  #1 class FastClass:    def do_stuff(self):        temp = self.value #           for i in range(10000):            ... #      `temp` #  #2 import random def fast_function():    r = random.random    for i in range(10000):        print(r()) #   `r()` ,     random.random() 

code将代码包装在函数中


该建议似乎与常识背道而驰,因为在调用函数时,某些数据被压入堆栈,并且系统在额外的负载处理下从该函数返回操作。 但是,此建议与先前的建议有关。 如果仅将所有代码放在一个文件中而不将其编写为函数,则由于使用全局变量,它的运行速度会慢得多。 这意味着可以通过将代码简单地包装在main()函数中并调用一次来加速代码:

 def main():    ... #  ,     main() 

▍请勿访问属性


可以降低程序速度的另一种机制是点( . )运算符,该运算符用于访问对象的属性。 该语句使用__getattribute__调用字典__getattribute__ ,这__getattribute__系统__getattribute__额外的压力。 如何限制此Python功能的性能影响?

 # : import re def slow_func():    for i in range(10000):        re.findall(regex, line) # ! # : from re import findall def fast_func():    for i in range(10000):        findall(regex, line) # ! 

strings提防琴弦


如果在循环中执行,字符串操作会大大降低程序的速度。 特别是,我们正在谈论使用%s.format()格式化字符串。 是否可以用某些东西代替它们? 如果查看Raymond Hettinger的最新推文 ,您会发现在这种情况下唯一需要使用的机制是f线。 这是最易读,简洁和最快的字符串格式化方法。 根据该推文,这里列出了可用于处理字符串的方法列表-从最快到最慢:

 f'{s} {t}' # ! s + ' ' + t ' '.join((s, t)) '%s %s' % (s, t) '{} {}'.format(s, t) Template('$s $t').substitute(s=s, t=t) # ! 

▍知道发电机也可以快速工作


生成器不是本质上快速的那些机制。 事实是,创建它们是为了执行“惰性”计算,这不仅节省了时间,而且节省了内存。 但是,节省内存可能会使程序运行得更快。 这怎么可能? 事实是,在不使用生成器(迭代器)的情况下处理大型数据集时,数据可能导致处理器的L1缓存溢出,这将大大减慢在内存中查找值的操作。

在性能方面,确保处理器能够快速访问其处理的数据以使其与数据尽可能接近非常重要。 这意味着此类数据应放置在处理器缓存中。 Raymond Hettinger在演示文稿中解决了此问题。

总结


优化的第一条规则是不需要优化。 但是,如果您不能没有优化,那么我希望我分享的技巧将对您有所帮助。

亲爱的读者们! 您如何处理优化Python代码性能的方法?

Source: https://habr.com/ru/post/zh-CN483678/


All Articles