对于编写事件驱动的模拟器,我依赖simpy,它大量使用Python生成器。我试图了解如何尽可能快地生成生成器,即最小化状态保存/恢复开销。我尝试了三种选择
并使用Python 3.4.3得到以下结果:
class_generator 20.851247710175812
global_generator 12.802394330501556
local_generator 9.067587919533253
可以找到代码here。
这对我来说是违反直觉的:在类实例中存储所有状态意味着只需要保存/恢复self
,而在全局存储所有状态应确保零保存/恢复开销。
有人知道为什么类生成器和全局生成器比本地生成器慢?
答案 0 :(得分:10)
当 yield 发生时,生成器实际上保留了实际的调用帧。无论你有1个还是100个局部变量,它都不会真正影响性能。
性能差异实际上来自于Python(我在这里使用的是CPython,也就是您从http://www.python.com/下载的那个,或者在您的操作系统上使用/usr/bin/python
,但大多数由于大多数相同的原因,实现具有类似的性能特征,表现在不同类型的变量查找上:
在Python中,局部变量实际上不是命名的;相反,它们由号引用,并由LOAD_FAST
操作码访问。
使用LOAD_GLOBAL
操作码访问全局变量。它们始终按名称引用,因此每次访问都需要实际的字典查找。
实例属性访问次数最慢,因为self.foobar
首先需要使用LOAD_FAST
加载对self
的引用,然后LOAD_ATTR
用于在引用的对象上找到foobar
,这是一个字典查找。此外,如果属性位于实例本身,则可以正常,但如果在类上设置,则属性查找将变慢。您还要在实例上设置值,它会更慢,因为现在它需要在加载的实例上执行STORE_ATTR
。更复杂的是,实例的类也需要参考 - 如果类碰巧有一个属性描述符同名,然后它可以改变阅读和设置属性的行为。
因此,最快的生成器是仅引用局部变量的生成器。 Python代码中常见的习惯用法是将全局只读变量的值存储到局部变量中以加快速度。
为了演示差异,请考虑为这3个变量访问生成的代码a
,b
和self.c
:
a = 42
class Foo(object):
def __init__(self):
self.c = 42
def foo(self):
b = 42
yield a
yield b
yield self.c
print(list(Foo().foo())) # prints [42, 42, 42]
foo
方法的反汇编的相关部分是:
8 6 LOAD_GLOBAL 0 (a)
9 YIELD_VALUE
10 POP_TOP
9 11 LOAD_FAST 1 (b)
14 YIELD_VALUE
15 POP_TOP
10 16 LOAD_FAST 0 (self)
19 LOAD_ATTR 1 (c)
22 YIELD_VALUE
23 POP_TOP
LOAD_GLOBAL
和LOAD_ATTR
的操作数分别是对名称a
和c
的引用;数字是桌子上的指数。 LOAD_FAST
的操作数是局部变量表表中局部变量的编号。
答案 1 :(得分:6)
生成器需要保存的唯一状态是对堆栈帧的引用,因此无论涉及多少状态以及放置数据的位置,保存和恢复状态都会花费完全相同的时间。
您在时间上看到的差异完全取决于Python可以访问值的速度:本地变量访问速度非常快,全局变量访问需要查找全局字典中的值,因此速度较慢,并且类成员访问需要访问本地变量' self'然后对该值执行至少一次字典查找(也必须将对类生成器的调用转换为具有单个参数的调用,该参数本身比没有参数的其他调用慢。)