11

代码跑得慢甩锅Python?手把手教你如何给代码提速30%

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MjM5MTQzNzU2NA%3D%3D&%3Bmid=2651676687&%3Bidx=3&%3Bsn=b15b6cfcb63a07ead743be62feaeb2e2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

fyymE3r.jpg!web

大数据文摘出品

来源: Medium

编译: 王转转

Python已经得到了全球程序员的喜爱,但是还是遭到一些人的诟病,原因之一就是认为它运行缓慢。

其实某个特定程序(无论使用何种编程语言)的运行速度是快还是慢,在很大程度上取决于编写该程序的开发人员自身素质,以及他们编写优化而高效代码的能力。

Medium上一位小哥就详细讲了讲如何让python提速30%,以此证明代码跑得慢不是python的问题,而是代码本身的问题。

时序分析

在开始进行任何优化之前,我们首先需要找出代码的哪些部分使整个程序变慢。 有时程序的问题很明显,但是如果你一时不知道问题出在哪里,那么这里有一些可能的选项:

注意: 这是我将用于演示的程序,它将进行指数计算(取自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 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 函数是罪魁祸首,现在我们可以更详细地了解时序和性能分析。

时序特定功能

现在我们知道了应当主要关注哪里,我们可能想对运行速度缓慢的函数计时,而不用测量其余的代码。 为此,我们可以使用一个简单的装饰器:

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_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_tim e两个函数。他们的区别在于 perf_counter 返回的绝对值,包括你的Python程序进程未运行时的时间,因此它可能会受到计算机负载的影响。另一方面, process_time 仅返回用户时间(不包括系统时间),这仅是你的过程时间。

加速吧!

让Python程序运行得更快,这部分会很有趣! 我不会展示可以解决你的性能问题的技巧和代码,更多地是关于构想和策略的,这些构想和策略在使用时可能会对性能产生巨大影响,在某些情况下,可以将速度提高30%。

使用内置数据类型

这一点很明显。 内置数据类型非常快,尤其是与我们的自定义类型(例如树或链接列表)相比。 这主要是因为内置程序是用C实现的,因此在使用Python进行编码时我们的速度实在无法与之匹敌。

使用 lru_cache 缓存/记忆

我已经在上一篇博客中展示了此内容,但我认为值得用简单的示例来重复它:

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秒钟,然后才返回结果。再次调用时,结果已经被缓存,因此它将跳过函数的主体并立即返回结果。有关更多实际示例,请参见以前的博客文章。

使用局部变量

这与在每个作用域中查找变量的速度有关,因为它不只是使用局部变量还是全局变量。实际上,即使在函数的局部变量(最快),类级属性(例如 self.name ——较慢)和全局(例如,导入的函数)如 time.time (最慢)之间,查找速度实际上也有所不同。

你可以通过使用看似不必要的分配来提高性能,如下所示:

# 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!

当心字符串

使用模数 (%s) .format() 进行循环运行时,字符串操作可能会变得非常慢。我们有什么更好的选择?根据雷蒙德·海廷格(Raymond Hettinger)最近的推特,我们唯一应该使用的是f字符串,它是最易读,最简洁且最快的方法。根据该推特,这是你可以使用的方法列表——最快到最慢:

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://towardsdatascience.com/making-python-programs-blazingly-fast-c1cd79bd1b32

uAVBzye.jpg!web

实习/全职编辑记者招聘ing

加入我们,亲身体验一家专业科技媒体采写的每个细节,在最有前景的行业,和一群遍布全球最优秀的人一起成长。坐标北京·清华东门,在大数据文摘主页对话页回复 “招聘” 了解详情。简历请直接发送至[email protected]

志愿者介绍

后台回复 志愿者 ”加入我们

7neaEnV.jpg!web

UziiMrJ.jpg!web

点「在看」的人都变好看了哦!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK