写点什么

如何使 Python 程序快如闪电,提速 30%?

  • 2020-01-17
  • 本文字数:3732 字

    阅读完需:约 12 分钟

如何使Python程序快如闪电,提速30%?

讨厌 Python 的人总是说,他们不想使用它的原因之一是它很慢。不管使用什么编程语言,程序是快还是慢都在很大程度上取决于编写程序的开发人员,以及他们编写最优化快速程序的技能和能力。在本文中,让我们来证明一下某些人的“误解”,看看如何提高 Python 程序的性能,使它们变得非常快!


本文最初发布于 martinheinz.dev 博客,经原作者授权由 InfoQ 中文站翻译并分享。

计时和性能分析

在我们开始优化任何东西之前,我们首先需要找出到底是代码的哪些部分减慢了整个程序。有时候,程序的瓶颈可能是显而易见的,但如果你不知道它在哪里,那么以下选项可以帮你找出来。


这是我将用于演示的程序,它计算 e 的 X 次方(摘自 Python 文档):


# slow_program.pyfrom 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 +sexp(Decimal(150))exp(Decimal(400))exp(Decimal(3000))
复制代码

最懒的“性能分析”

首先是最简单同时又非常懒惰的解决方案——Unix time 命令:


~ $ time python3.8 slow_program.pyreal  0m11,058suser  0m11,050ssys   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 函数是罪魁祸首(惊喜!),现在我们可以得到更具体的时间和性能分析…

对具体的函数计时

现在我们知道了应该将注意力放在哪里,我们可能希望对慢速函数进行计时,而不需要测量代码的其余部分。我们可以使用简单的装饰器:


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
复制代码


接下来,可以把这个装饰器应用到函数上,像下面这样:


@timeit_wrapperdef 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.pymodule     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_cache 缓存数据

我已经在之前的博文中介绍过这个,但是我认为值得通过一个简单的例子再说明一下:


import functoolsimport time# 最多缓存12个不同的结果@functools.lru_cache(maxsize=12)def slow_func(x):    time.sleep(2)  # 模拟长时间计算    return xslow_func(1)  # ... 等待2秒才能获得结果slow_func(1)  # 结果已缓存,会立即返回slow_func(3)  # ... 等待2秒才能获得结果
复制代码


上面的函数使用 time.sleep 模拟大量计算。第一次使用参数 1 调用时,它将等待 2 秒,然后才返回结果。当再次调用时,结果已经被缓存,因此,它会跳过函数体并立即返回结果。要了解更多真实的例子,请点击这里查看以前的博文。

使用局部变量

这与在每个作用域内查找变量的速度有关。我会写每个作用域,因为它不只关乎使用局部变量还是全局变量。查找速度也确实存在差异,函数中的局部变量最快,类级属性(例如 self.name)次之,而全局(例如导入的函数 time.time)变量最慢。


你可以像下面这样,使用不必要的赋值来提升性能:


#  示例#1class FastClass:    def do_stuff(self):        temp = self.value  # 这可以加速循环中的查找        for i in range(10000):            ...  # 在这里使用`temp`做些操作#  示例#2import randomdef fast_function():    r = random.random    for i in range(10000):        print(r())  # 在这里调用`r()`,比全局的random.random()要快
复制代码

使用函数

这看起来可能不符合直觉,因为调用函数会将更多的东西放到堆栈中,从函数返回时会产生开销,但这与前面一点有关。如果你只是将整个代码放入一个文件中,而不将其放入函数中,那么由于全局变量的关系,速度会慢很多。因此,你只是将整个代码封装在 main 函数中并调用一次,就可以加快你的代码,像这样:


def main():    ...  # 之前所有的全局代码main()
复制代码

不要访问属性

另一个可能降低程序速度的是点操作符(.),它可以用于访问对象属性。这个操作符使用_getattribute__触发字典查找,这会在代码中产生额外的开销。那么,我们如何才能避免(限制)使用它呢?


#  慢:import redef slow_func():    for i in range(10000):        re.findall(regex, line)  # 慢!#  快:from re import findalldef fast_func():    for i in range(10000):        findall(regex, line)  # 较快!
复制代码

提防字符串

在循环中运行诸如模数(%s)或.format()之类的方法时,对字符串的操作可能会变得非常慢。我们还有什么更好的选择吗?根据 Raymond Hettinger 最近的推文,我们唯一应该使用的是 f-string,它是最易读、最简洁、最快速的方法。因此,根据那条推文,你可以使用以下方法——从最快的到最慢的:


f'{s} {t}'  # 快!s + '  ' + t ' '.join((s, t))'%s %s' % (s, t) '{} {}'.format(s, t)Template('$s $t').substitute(s=s, t=t)  # 慢!
复制代码


生成器本身并没有更快,因为它们允许延迟计算,这节省的是内存而不是时间。但是,节省的内存可能会使得程序在实际运行时更快。为什么?如果你有一个大型数据集,并且没有使用生成器(迭代器),那么数据可能会溢出 CPU L1 缓存,这将显著降低在内存中查找值的速度。


说到性能,很重要的一点是 CPU 可以将它正在处理的所有数据保存在缓存中。你可以看下Raymond Hettingers的演讲,他提到了这些问题。

小结

优化的第一原则是不做优化。但是,如果你真的需要,我希望这些小技巧能帮到你。不过,在优化代码时要注意,因为它可能会使代码难于阅读、难于维护,甚至超过优化带来的好处。


原文链接:


https://martinheinz.dev/blog/13


2020-01-17 08:016151
用户头像
刘燕 InfoQ高级技术编辑

发布了 1112 篇内容, 共 583.5 次阅读, 收获喜欢 1981 次。

关注

评论

发布
暂无评论
发现更多内容

数据集成和分析时,应该注意哪些关键因素?

Aloudata

数据分析 数据虚拟化 数据编织

JimuReport 积木报表 v1.9.3发布,免费可视化报表

JEECG低代码

数据可视化 报表 报表工具 报表打印 大屏设计器

Set A Light 3D Studio:打造专业级3D灯光布局,摄影布光从此得心应手

Rose

企业流程自动化:AI技术加持,解锁高效运营

合合技术团队

人工智能 AI 自动化 数据治理

转转平台IM系统架构设计与实践(一):整体架构设计

JackJiang

即时通讯;IM;网络编程

BOE(京东方)全新概念级“AI视听中心”亮相CES 2025 携手高通引领智慧家居娱乐新图景

爱极客侠

WebGL开发虚拟解剖系统

北京木奇移动技术有限公司

软件外包公司 webgl开发 医学解剖

语音技术与人工智能:智能语音交互的多场景应用探索

天津汇柏科技有限公司

AI 人工智能

代码编辑器 sublime text破解安装包 附sublime text汉化补丁

Rose

AIP智能体平台:赋能软件开发与运行的新时代

大东(AIP智能体运营专员)

人工智能

WebGL在医学领域的应用

北京木奇移动技术有限公司

软件外包公司 webgl开发 医学领域

一个测试Leader的工作案例

老张

团队管理 项目管理 软件测试 自动化测试

企业如何实现多源异构数据的统一化管理?

Aloudata

数据分析 数据管理 数据集成 数据虚拟化 数据编织

pd19虚拟机永久许可证 Parallels Desktop 19下载安装

Rose

Java分析工具 JProfiler for mac注册码及安装教程

Rose

KubeEdge SIG AI 产业革新与应用:跨越边界,迎向未来

华为云原生团队

云计算 容器 云原生

苹果Mac远程管理Royal TSX使用教程

Rose

MestReNova 14:精准解析NMR数据,助力科研突破!

Rose

【连载 10】CountDownLatch

FunTester

Fluent Editor v3.25.0 正式发布!2025年第一个版本,增加标题列表导航、分隔线、多图多文件上传等实用特性

OpenTiny社区

开源 前端 富文本 OpenTiny

AIP智能体平台:构建未来工作的智慧管家

大东(AIP智能体运营专员)

人工智能

苹果Mac版SVN客户端 SnailSVN Pro 免激活版

Rose

Typeeto for Mac:让Mac键盘秒变蓝牙键盘,跨设备输入更高效

Rose

观测云产品更新 | 日志索引、Pipeline、场景图表等优化

观测云

产品迭代

iZotope音乐制作 Ozone Advanced 11直装版下载安装

Rose

定制 CRM 的4个真实用户案例(帮你选择更灵活CRM产品)

NocoBase

开源 CRM 定制化 CRM系统 案例研究

如何使Python程序快如闪电,提速30%?_AI&大模型_Martin Heinz_InfoQ精选文章