【问题标题】:Python's [<generator expression>] at least 3x faster than list(<generator expression>)?Python [<generator expression>] 至少比 list(<generator expression>) 快 3 倍?
【发布时间】:2011-05-23 02:49:44
【问题描述】:

似乎在生成器表达式 (test1) 周围使用 [] 比将它放在 list() 中 (test2) 表现得更好。当我只是将一个列表传递给 list() 以进行浅拷贝(test3)时,速度并不慢。这是为什么呢?

证据:

from timeit import Timer

t1 = Timer("test1()", "from __main__ import test1")
t2 = Timer("test2()", "from __main__ import test2")
t3 = Timer("test3()", "from __main__ import test3")

x = [34534534, 23423523, 77645645, 345346]

def test1():
    [e for e in x]

print t1.timeit()
#0.552290201187


def test2():
    list(e for e in x)

print t2.timeit()
#2.38739395142

def test3():
    list(x)

print t3.timeit()
#0.515818119049

机器:64 位 AMD、Ubuntu 8.04、Python 2.7 (r27:82500)

【问题讨论】:

  • 您的列表中只有 4 (!!) 个整数。您测量的时间可能主要是创建生成器等。
  • 请注意,如果您使 x 甚至 稍微 大一些,例如 range(100),结果会发生巨大变化:t2 仅比 t1 慢 50%,而 t3 则遥遥领先。
  • test2 示例是生成器表达式转换为列表,而不是列表理解
  • 我什至不会考虑前两个选项。它们只是冗余且更长(本质上,就像 Haskell 中的 map id 一样,只是该语言具有智能编译器)。
  • Python 在不执行程序的情况下无法确定 list 是内置的 list 函数。另一方面,[ .. ] 将始终是一个列表,因此可以对其进行优化。

标签: python performance profiling


【解决方案1】:

在 python 中,list 必须在模块中查找名称,然后在内置函数中查找。虽然您无法更改列表理解的含义,但列表调用必须只是标准查找 + 函数调用,因为它可以重新定义为其他内容。

查看为理解生成的 vm 代码可以看出它是内联的,而对 list 的调用是正常调用。

>>> import dis
>>> def foo():
...     [x for x in xrange(4)]
... 
>>> dis.dis(foo)
  2           0 BUILD_LIST               0
              3 DUP_TOP             
              4 STORE_FAST               0 (_[1])
              7 LOAD_GLOBAL              0 (xrange)
             10 LOAD_CONST               1 (4)
             13 CALL_FUNCTION            1
             16 GET_ITER            
        >>   17 FOR_ITER                13 (to 33)
             20 STORE_FAST               1 (x)
             23 LOAD_FAST                0 (_[1])
             26 LOAD_FAST                1 (x)
             29 LIST_APPEND         
             30 JUMP_ABSOLUTE           17
        >>   33 DELETE_FAST              0 (_[1])
             36 POP_TOP             
             37 LOAD_CONST               0 (None)
             40 RETURN_VALUE        

>>> def bar():
...     list(x for x in xrange(4))
... 
>>> dis.dis(bar)
  2           0 LOAD_GLOBAL              0 (list)
              3 LOAD_CONST               1 (<code object <genexpr> at 0x7fd1230cf468, file "<stdin>", line 2>)
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              1 (xrange)
             12 LOAD_CONST               2 (4)
             15 CALL_FUNCTION            1
             18 GET_ITER            
             19 CALL_FUNCTION            1
             22 CALL_FUNCTION            1
             25 POP_TOP             
             26 LOAD_CONST               0 (None)
             29 RETURN_VALUE  

【讨论】:

  • 可能的翻译:“可能需要查找名称'list'。您无法更改列表理解的含义(因为它是语言语法的一部分),我认为列表已查找up(在运行时)就像任何其他变量一样,然后作为函数调用,因为它可以分配给 'list'。"
【解决方案2】:

list(e for e in x) 不是列表解析,它是一个 genexpr 对象 (e for e in x) 正在创建并传递给 list 工厂函数。据推测,对象创建和方法调用会产生开销。

【讨论】:

    【解决方案3】:

    嗯,我的第一步是独立设置这两个测试,以确保这不是例如定义函数的顺序。

    >python -mtimeit "x=[34534534, 23423523, 77645645, 345346]" "[e for e in x]"
    1000000 loops, best of 3: 0.638 usec per loop
    
    >python -mtimeit "x=[34534534, 23423523, 77645645, 345346]" "list(e for e in x)"
    1000000 loops, best of 3: 1.72 usec per loop
    

    果然,我可以复制这个。好的,下一步是查看字节码以了解实际情况:

    >>> import dis
    >>> x=[34534534, 23423523, 77645645, 345346]
    >>> dis.dis(lambda: [e for e in x])
      1           0 LOAD_CONST               0 (<code object <listcomp> at 0x0000000001F8B330, file "<stdin>", line 1>)
                  3 MAKE_FUNCTION            0
                  6 LOAD_GLOBAL              0 (x)
                  9 GET_ITER
                 10 CALL_FUNCTION            1
                 13 RETURN_VALUE
    >>> dis.dis(lambda: list(e for e in x))
      1           0 LOAD_GLOBAL              0 (list)
                  3 LOAD_CONST               0 (<code object <genexpr> at 0x0000000001F8B9B0, file "<stdin>", line 1>)
                  6 MAKE_FUNCTION            0
                  9 LOAD_GLOBAL              1 (x)
                 12 GET_ITER
                 13 CALL_FUNCTION            1
                 16 CALL_FUNCTION            1
                 19 RETURN_VALUE
    

    请注意,第一个方法直接创建列表,而第二个方法创建一个genexpr 对象并将其传递给全局list。这可能就是开销所在。

    另请注意,差异大约是一微秒,即完全微不足道。


    其他有趣的数据

    这仍然适用于非平凡的列表

    >python -mtimeit "x=range(100000)" "[e for e in x]"
    100 loops, best of 3: 8.51 msec per loop
    
    >python -mtimeit "x=range(100000)" "list(e for e in x)"
    100 loops, best of 3: 11.8 msec per loop
    

    对于不那么琐碎的地图功能:

    >python -mtimeit "x=range(100000)" "[2*e for e in x]"
    100 loops, best of 3: 12.8 msec per loop
    
    >python -mtimeit "x=range(100000)" "list(2*e for e in x)"
    100 loops, best of 3: 16.8 msec per loop
    

    如果我们过滤列表(虽然不那么强烈):

    >python -mtimeit "x=range(100000)" "[e for e in x if e%2]"
    100 loops, best of 3: 14 msec per loop
    
    >python -mtimeit "x=range(100000)" "list(e for e in x if e%2)"
    100 loops, best of 3: 16.5 msec per loop
    

    【讨论】:

    • 大问题的方法和解释
    • 好吧,即使对于大型随机 ists,它确实需要更长的百分比,所以它确实可能成为一个问题。当然,无论谁写 list(i for i in it) 而不是 list(it) 或(在 sliceables 的情况下)it[:] 都不应该首先编写关键代码;)我也相信 genexprs 的懒惰(必须在某个地方进行管理,当然)增加了一些开销。
    • 注意:正如上面的 THC4K 所说,list 可能会被隐藏 (list = None) 的事实可能会阻止第二个版本中可能发生的任何优化。
    • 开销不会随着列表变长而消失的原因是,很多开销在于暂停和恢复生成器的成本,它与序列中的项目数量成比例。列表理解形式只是直接构建结果列表,不需要暂停或恢复。要真正深入研究这一点,请查看 lambda 函数上的 .func_code.co_consts[0] 属性的 dis 结果(Python 3 中的 .__code__.co_consts[0])。
    【解决方案4】:

    你的 test2 大致相当于:

    def test2():
        def local():
            for i in x:
                yield i
        return list(local())
    

    调用开销解释了处理时间的增加。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多