【问题标题】:For loop item unpackingFor循环项目拆包
【发布时间】:2014-05-27 04:43:01
【问题描述】:

有一次,在看了 Mike Muller 的性能优化教程(我认为是this one)之后,我开始想到一个想法:如果性能很重要,请尽量减少按索引访问循环中的项目,例如。 G。如果您需要在循环中多次访问x[1] for x in l - 将变量分配给x[1] 并在循环中重用它。

现在我有了这个合成示例:

import timeit

SEQUENCE = zip(range(1000), range(1, 1001))

def no_unpacking():
    return [item[0] + item[1] for item in SEQUENCE]


def unpacking():    
    return [a + b for a, b in SEQUENCE]


print timeit.Timer('no_unpacking()', 'from __main__ import no_unpacking').timeit(10000)
print timeit.Timer('unpacking()', 'from __main__ import unpacking').timeit(10000)

unpacking()no_unpacking() 函数返回相同的结果。实现方式不同:unpacking() 将项目解包到循环中的abno_unpacking() 通过索引获取值。

对于python27,它显示:

1.25280499458
0.946601867676

换句话说,unpacking() 的性能优于 no_unpacking() 约 25%。

问题是:

  • 为什么按索引访问会显着减慢速度(即使在这种简单的情况下)?

额外问题:

  • 我也在pypy 上试过这个——这两个函数在性能方面几乎没有区别。这是为什么呢?

感谢您的帮助。

【问题讨论】:

    标签: python performance for-loop iterable-unpacking


    【解决方案1】:

    为了回答您的问题,我们可以使用dis 模块检查这两个函数生成的字节码:

    In [5]: def no_unpacking():
       ...:     s = []
       ...:     for item in SEQUENCE:
       ...:         s.append(item[0] + item[1])
       ...:     return s
       ...: 
       ...: 
       ...: def unpacking():  
       ...:     s = []
       ...:     for a,b in SEQUENCE:
       ...:         s.append(a+b)  
       ...:     return s
    

    我已经扩展了列表理解,因为在 python3 中检查有趣的字节码会更麻烦。代码是等价的,所以它对我们的目的并不重要。

    第一个函数的字节码是:

    In [6]: dis.dis(no_unpacking)
      2           0 BUILD_LIST               0 
                  3 STORE_FAST               0 (s) 
    
      3           6 SETUP_LOOP              39 (to 48) 
                  9 LOAD_GLOBAL              0 (SEQUENCE) 
                 12 GET_ITER             
            >>   13 FOR_ITER                31 (to 47) 
                 16 STORE_FAST               1 (item) 
    
      4          19 LOAD_FAST                0 (s) 
                 22 LOAD_ATTR                1 (append) 
                 25 LOAD_FAST                1 (item) 
                 28 LOAD_CONST               1 (0) 
                 31 BINARY_SUBSCR        
                 32 LOAD_FAST                1 (item) 
                 35 LOAD_CONST               2 (1) 
                 38 BINARY_SUBSCR        
                 39 BINARY_ADD           
                 40 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
                 43 POP_TOP              
                 44 JUMP_ABSOLUTE           13 
            >>   47 POP_BLOCK            
    
      5     >>   48 LOAD_FAST                0 (s) 
                 51 RETURN_VALUE      
    

    请注意,循环必须调用BINARY_SUBSCR 两次才能访问元组的两个元素。

    第二个函数的字节码是:

    In [7]: dis.dis(unpacking)
      9           0 BUILD_LIST               0 
                  3 STORE_FAST               0 (s) 
    
     10           6 SETUP_LOOP              37 (to 46) 
                  9 LOAD_GLOBAL              0 (SEQUENCE) 
                 12 GET_ITER             
            >>   13 FOR_ITER                29 (to 45) 
                 16 UNPACK_SEQUENCE          2 
                 19 STORE_FAST               1 (a) 
                 22 STORE_FAST               2 (b) 
    
     11          25 LOAD_FAST                0 (s) 
                 28 LOAD_ATTR                1 (append) 
                 31 LOAD_FAST                1 (a) 
                 34 LOAD_FAST                2 (b) 
                 37 BINARY_ADD           
                 38 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
                 41 POP_TOP              
                 42 JUMP_ABSOLUTE           13 
            >>   45 POP_BLOCK            
    
     12     >>   46 LOAD_FAST                0 (s) 
                 49 RETURN_VALUE 
    

    注意没有BINARY_SUBSCR 可以执行。

    所以,看起来UNPACK_SEQUENCE 加上一个STORE_FAST(这是解包添加的额外操作)更快然后执行两个BINARY_SUBSCR。 这是合理的,因为BINARY_SUBSCR 是一个成熟的方法调用,而UNPACK_SEQUENCESTORE_FAST 是更简单的操作。

    即使在更简单的情况下,您也可以看到差异:

    In [1]: def iter_with_index(s):
       ...:     for i in range(len(s)):
       ...:         s[i]
       ...:         
    
    In [2]: def iter_without_index(s):
       ...:     for el in s:el
       ...:     
    
    In [3]: %%timeit s = 'a' * 10000
       ...: iter_with_index(s)
       ...: 
    1000 loops, best of 3: 583 us per loop
    
    In [4]: %%timeit s = 'a' * 10000
       ...: iter_without_index(s)
       ...: 
    1000 loops, best of 3: 206 us per loop
    

    正如您所见,使用显式索引对字符串进行迭代大约慢了 3 倍。 由于调用BINARY_SUBSCR,这都是开销。

    关于您的第二个问题:pypy 具有能够分析代码并生成优化版本的 JIT,从而避免了索引操作的开销。 当它意识到订阅是在元组上完成时,它可能能够生成不调用元组方法但直接访问元素的代码,从而完全删除BINARY_SUBSCR 操作。

    【讨论】:

    • 很好的解释,很高兴知道我没有理由担心。
    • 很好的答案。非常彻底。
    • 非常好的解释。但是,只需将range 更改为xrange,我的笔记本上的速度就会从 4.0 us 变为 3.43 us
    猜你喜欢
    • 1970-01-01
    • 2017-08-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多