我最近一直在努力创建一个素数查找程序。但是,我注意到,当使用参数时,一个函数比使用预设值时慢得多。
在3个不同的版本中,很明显变量显着减慢了程序的速度,我想知道原因。
这是原始的(这个问题有点简化)功能:
def version1(n, p):
return ((n*n - 2) & ((1 << p) - 1)) + ((n*n - 2) >> p)
使用timeit
模块运行100次:
timeit.timeit("version1(200, 500000000)", "from __main__ import version1", number=100)
需要7.5
秒。
但是,这是第二个版本,其中没有参数,并且数字直接放在返回值中。该方程与 Version 1 :
完全相同def version2():
return ((200*200 - 2) & ((1 << 500000000) - 1)) + ((200*200 - 2) >> 500000000)
使用timeit
模块运行100次:
timeit.timeit("version2()", "from __main__ import version2", number=100
只需要0.00001
秒!
最后,为了完整起见,我尝试了一个没有参数但仍将其值保存为变量的版本:
def version3():
n = 200
p = 500000000
return ((n*n - 2) & ((1 << p) - 1)) + ((n*n - 2) >> p)
使用timeit
:
timeit.timeit("version3()", "from __main__ import version3", number = 100)
花了6.3
秒,相对接近版本1 。
为什么在涉及变量时,相同的功能可以花费更长的时间,以及如何使版本1 更有效?
答案 0 :(得分:26)
Python在编译为所谓的peep-hole optimisation时预先计算计算:
>>> import dis
>>> def version2():
... return ((200*200 - 2) & ((1 << 500000000) - 1)) + ((200*200 - 2) >> 500000000)
...
>>> dis.dis(version2)
2 0 LOAD_CONST 13 (39998)
2 RETURN_VALUE
version2()
返回已经计算的值,并执行无实际工作。当然,返回常数比每次计算值要快得多。
有关编译器如何执行此操作的详细信息,请参阅peephole.c
Python源文件中的fold_binops_on_constants
function。
因此,编译version2
需要(很多)时间比version1
多:
>>> import timeit
>>> version1_text = '''\
... def version1(n, p):
... return ((n*n - 2) & ((1 << p) - 1)) + ((n*n - 2) >> p)
... '''
>>> version2_text = '''\
... def version2():
... return ((200*200 - 2) & ((1 << 500000000) - 1)) + ((200*200 - 2) >> 500000000)
... '''
>>> timeit.timeit("compile(t, '', 'exec')", 'from __main__ import version1_text as t', number=10)
0.00028649598243646324
>>> timeit.timeit("compile(t, '', 'exec')", 'from __main__ import version2_text as t', number=10)
2.2103765579813626
好东西Python缓存编译的字节码结果!
每个子表达式的中间结果也存储在代码对象的co_consts
属性中,其中一些相当大:
>>> import sys
>>> consts = version2.__code__.co_consts
>>> for obj in consts:
... size = sys.getsizeof(obj)
... print(f'{type(obj)!s:<18} {size:<8} {"<too large to print>" if size > 100 else obj}')
...
<class 'NoneType'> 16 None
<class 'int'> 28 200
<class 'int'> 28 2
<class 'int'> 28 1
<class 'int'> 28 500000000
<class 'int'> 28 40000
<class 'int'> 28 39998
<class 'int'> 66666692 <too large to print>
<class 'int'> 66666692 <too large to print>
<class 'int'> 28 39998
<class 'int'> 28 40000
<class 'int'> 28 39998
<class 'int'> 24 0
<class 'int'> 28 39998
所以这确实使字节码缓存更大了:
>>> import marshal
>>> len(marshal.dumps(version1.__code__))
129
>>> len(marshal.dumps(version2.__code__))
133333481
对于包含非参数版本的模块,.pyc
文件至少为127MB!
答案 1 :(得分:4)
正如Martijn的深入回答指出的那样,version2
是由于CPython执行不断折叠而变得更快的,变量的数量并没有发挥作用。
对于version3
与version1
,version3
更快,因为在调用version1
时执行了额外的参数解析。
Python需要为每次调用执行此解析。当你计时并重复调用100次时,这种差异(其他不太明显)会显示出来。如果您将参数定义为**kwargs
,*args
或使用默认值,那么您将获得略微不同的时序结果。