写点什么

如何使 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:015848
用户头像
刘燕 InfoQ高级技术编辑

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

关注

评论

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

百度搜索中台海量数据管理的云原生和智能化实践

百度Geek说

架构 云原生 后端 百度搜索

MySQL数据库升级后如何防止性能下降

@零度

MySQL

公安情报研判系统开发,情报可视化分析研判平台解决方案

电微13828808271

【教程直播第4期】揭秘数据迁移之 OceanBase CDC & OMS 社区版能力

OceanBase 数据库

oceanbase OceanBase 开源 OceanBase 社区版

volatile和synchronzied的区别

悟空聊架构

synchronized 28天写作 悟空聊架构 12月日更 volatitle

【北京讲座报名!】12月26日与深度学习大牛共赋技术之约!

亚马逊云科技 (Amazon Web Services)

人工智能 深度学习 活动 ML

医疗的客户体验

张老蔫

28天写作

带你尝鲜LiteOS 组件EasyFlash

华为云开发者联盟

LiteOS 组件 EasyFlash 嵌入式闪存库 Flash

恒源云(GPUSHARE)_LLD: 内部数据指导的标签去噪方法【ACL 2022】

恒源云

人工智能 深度学习

一文搞懂TopK问题

bigsai

数据结构 算法

大数据之面试篇之Hadoop/HDFS/Yarn

@零度

大数据 面试题

养孩子到底有多难?

Tiger

28天写作

智慧社区综合服务管理平台,社区管理系统搭建

电微13828808271

react源码解析14.手写hooks

buchila11

React

30个类手写Spring核心原理之自定义ORM(上)(6)

Tom弹架构

Java spring 源码

Linux一学就会之Linux系统启动原理及故障排除

学神来啦

Linux 运维 黑客 linux运维 linux云计算

Kafka-Broker的基本模块

编程江湖

大数据

跳出问题限制来解决问题-cacerts证书库的证书丢失的解决

superman

跳出问题解决问题 命题似解决 cacerts证书库

Java开发之命名规范

@零度

Java 命名规范

云服务器ECS年终特惠,老用户新购优惠低至4折

阿里云弹性计算

云服务器 年终特惠

拍乐云测试自动化实践

拍乐云Pano

DevOps 敏捷开发 自动化测试

资本巨头纷纷入局,DAO究竟有何魔力?

旺链科技

区块链 组织 DAO

重点人员动态管控系统开发,智慧公安预警管控平台

电微13828808271

在线JSON转sarcastic工具

入门小站

工具

记字节前端面试一道简单的算法题

全栈潇晨

LeetCode 算法面试

Java 泛型通配符详解

编程江湖

JAVA开发 java编程

Vue 路由组件传参的 8 种方式

编程江湖

Vue 前端开发

Linux之mv命令

入门小站

Linux

react源码解析13.hooks源码

buchila11

React

DataPipeline实时数据融合产品入驻青云云市场,催化企业数据价值释放

DataPipeline数见科技

大数据 中间件 Big Data 数据融合 数据管理

dart系列之:dart优秀的秘诀-隔离机制

程序那些事

flutter dart 程序那些事 12月日更 flutter for web

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