HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

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

关注

评论

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

JavaScript 基础知识

喜羊羊

9月月更

中秋节,华为云AI送上超级大月亮制作教程,体验赢开发者键鼠套装

华为云开发者联盟

人工智能 华为云 中秋节 企业号九月金秋榜

【C语言深度剖析】详解strlen与sizeof的区别及用法

Albert Edison

C语言 sizeof 9月月更 strlen

数据治理(十一):数据安全管理Ranger初步认识

Lansonli

数据治理 9月月更

Qt|自定义Widget实现互斥效果问题

中国好公民st

qt QWidget 9月月更

Sprint Review能不能做成Demo演示?

LigaAI

Scrum 敏捷开发 迭代增量开发 高效办公 企业号九月金秋榜

Linux系统安装Redis

Centos 7 redis 底层原理 9月月更

讲究卡路里多少的美食出圈了!维小饭被评为“2022中国轻食十大品牌”

联营汇聚

库调多了,都忘了最基础的概念 <锁与线程篇1>

知识浅谈

线程 9月月更

面试突击:什么是跨域问题?如何解决?

Java快了!

java;

【JavaWeb】Servlet系列——使用纯Servlet做一个单表的CRUD操作

胖虎不秃头

Web java; 9月月更

4天带你上手HarmonyOS ArkUI开发

HarmonyOS开发者

HarmonyOS

【活动预告】数据集成海外专场Meetup:走进Shopee,聊透SeaTunnel优化实践

Apache SeaTunnel

技术分享 数据同步 数据集成 社区活动

nft系统开发

开源直播系统源码

区块链 NFT 数字藏品 数字藏品软件开发

JavaWeb -JavaBean MVC Filter 监听器 过滤器

喜羊羊

9月月更

为什么越来越多博士逃离科研?

博文视点Broadview

计网复习二,网络应用

前端小刘不怕牛牛

计算机网络 HTTP 9月月更

VUE 项目本地没有问题,部署到服务器上提示错误

HoneyMoose

[Go WebSocket] 多房间的聊天室(一)思考篇

HullQin

Go golang 后端 websocket 9月月更

云数据库技术|“重磅升级”后再测TDSQL-C

数据库 polarDB 玖章算术 TDSQL-C

如何让开发者直接在应用后台控制用户的运动状态?

HarmonyOS SDK

Zilliz 论文入选数据库顶会 VLDB'22

Zilliz

数据库 分布式 云原生 VLDB'22

客户案例|宜泊科技怎样实现智慧停车可观测

观测云

数据科学家、数据工程师和数据分析师三个角色的区别是什么

雨果

数据分析师 数据科学 数据工程师

【JavaWeb】Servlet系列——HttpServletRequest接口详解

胖虎不秃头

Web java; 9月月更

ChaosBlade Java 场景性能优化,那些你不知道的事

Java快了!

java;

调用 sap.ui.base.ManagedObject 的构造函数时,如何传递绑定路径进去

汪子熙

JavaScript SAP SAP UI5 ui5 9月月更

TDengine支持多种写入协议,四种写入方式提效大全

TDengine

tdengine 开源 时序数据库 企业号九月金秋榜

Java终极学习路线-共计9大模块/6大框架/13个中间件

小明Java问道之路

Java 架构 JVM 中间件 9月月更

javaweb-JSP

喜羊羊

9月月更

Java 多线程:基础

Java快了!

java;

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