【问题标题】:How to make Python generators as fast as possible?如何使 Python 生成器尽可能快?
【发布时间】:2016-06-07 09:07:34
【问题描述】:

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

  1. 所有状态都存储在一个类实例中
  2. 全局存储所有状态
  3. 本地存储的所有状态

并使用 Python 3.4.3 得到以下结果:

class_generator 20.851247710175812
global_generator 12.802394330501556
local_generator 9.067587919533253

代码可以在here找到。

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

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

【问题讨论】:

    标签: python python-3.x generator


    【解决方案1】:

    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-反对,这是一个字典查找。此外,如果属性在 instance 本身上,这将没问题,但如果在 class 上设置,则属性查找会变慢。您还在实例上设置值,它会更慢,因为现在它需要在加载的实例上执行STORE_ATTR。更复杂的是实例的 class 也需要被查询 - 如果 class 碰巧有一个 property descriptor同名,则可以改变读取和设置属性的行为。

    因此最快的生成器是只引用局部变量的生成器。在 Python 代码中,将全局只读变量的值存储到局部变量中以加快速度是 Python 代码中的常见习语。

    为了演示差异,请考虑为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的操作数是局部变量表中局部变量的编号

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

      • 与迭代生成器相比,我认为调用类生成器不会产生任何可观的成本。调用只需要进行一次(当生成器被实例化时),所有三个生成器都一样,调用它的__next__ 方法。类生成器是最慢的,因为对实例的属性查找相对复杂(Python 在检查实例是否具有属性之前必须检查类上是否存在数据描述符)。
      猜你喜欢
      • 2011-11-17
      • 1970-01-01
      • 2014-01-17
      • 1970-01-01
      • 1970-01-01
      • 2023-03-31
      • 2016-04-16
      • 2015-09-25
      • 1970-01-01
      相关资源
      最近更新 更多