如何尽可能快地制作Python生成器?

时间:2016-06-07 09:07:35

标签: python python-3.x generator

对于编写事件驱动的模拟器,我依赖simpy,它大量使用Python生成器。我试图了解如何尽可能快地生成生成器,即最小化状态保存/恢复开销。我尝试了三种选择

  1. 存储在类实例中的所有状态
  2. 全部存储全州
  3. 本地存储的所有州
  4. 并使用Python 3.4.3得到以下结果:

    class_generator 20.851247710175812
    global_generator 12.802394330501556
    local_generator 9.067587919533253
    

    可以找到代码here

    这对我来说是违反直觉的:在类实例中存储所有状态意味着只需要保存/恢复self,而在全局存储所有状态应确保零保存/恢复开销。

    有人知道为什么类生成器和全局生成器比本地生成器慢?

2 个答案:

答案 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个变量访问生成的代码abself.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_GLOBALLOAD_ATTR的操作数分别是对名称ac的引用;数字是桌子上的指数。 LOAD_FAST的操作数是局部变量表表中局部变量的编号。

答案 1 :(得分:6)

生成器需要保存的唯一状态是对堆栈帧的引用,因此无论涉及多少状态以及放置数据的位置,保存和恢复状态都会花费完全相同的时间。

您在时间上看到的差异完全取决于Python可以访问值的速度:本地变量访问速度非常快,全局变量访问需要查找全局字典中的值,因此速度较慢,并且类成员访问需要访问本地变量' self'然后对该值执行至少一次字典查找(也必须将对类生成器的调用转换为具有单个参数的调用,该参数本身比没有参数的其他调用慢。)