替换列表中的元素与列表理解的性能

时间:2017-01-17 17:39:09

标签: python performance list-comprehension disassembly measurement

正如user2357112指出的那样,原始的性能测试脚本并没有像我预期的那样工作。 "在第一次执行s1之后,你的列表中没有1,因此没有进一步执行s1并且没有执行s2实际上采用x == 1分支。" 修改后的版本:

import timeit
import random
random.seed(0)
a = [ random.randrange(10) for _ in range(10000)]
change_from = 1
change_to = 6
setup = "from __main__ import a, change_from, change_to"

# s1 is replaced for a simple for loop, which is faster than the original
s1 = """\
for i,x in enumerate(a):
    if x == change_from:
        a[i] = change_to
change_from, change_to = change_to, change_from
"""

s2 = """\
a = [change_to if x==change_from else x for x in a]
change_from, change_to = change_to, change_from
"""
print(timeit.timeit(stmt=s1,number=10000, setup=setup))
print(timeit.timeit(stmt=s2, number=10000, setup=setup))

此脚本替换每次出现的1到6,然后下一次出现每次出现的6到1.依此类推。结果是:

7.841739330212443
5.5166219217914065

为什么列表理解更快?

如何找出这类问题呢? 董事会的评论看起来很有意思,谢谢。

使用以下python版本: 在win32上的Python 3.6.0(v3.6.0:41df79263a11,2016年12月23日,08:06:12)[MSC v.1900 64位(AMD64)]

由于我没有得到详细的答案,我试图弄明白。

如果我使用此函数表示简单的for循环:

def func1(a):
    for i,x in enumerate(a):
        if x == 1:
            a[i] = 6
    return(a)

并拆解它,我得到以下内容:

func1:
  7           0 SETUP_LOOP              48 (to 51)
              3 LOAD_GLOBAL              0 (enumerate)
              6 LOAD_FAST                0 (a)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 GET_ITER
        >>   13 FOR_ITER                34 (to 50)
             16 UNPACK_SEQUENCE          2
             19 STORE_FAST               1 (i)
             22 STORE_FAST               2 (x)

  8          25 LOAD_FAST                2 (x)
             28 LOAD_CONST               1 (1)
             31 COMPARE_OP               2 (==)
             34 POP_JUMP_IF_FALSE       13

  9          37 LOAD_CONST               2 (6)
             40 LOAD_FAST                0 (a)
             43 LOAD_FAST                1 (i)
             46 STORE_SUBSCR
             47 JUMP_ABSOLUTE           13
        >>   50 POP_BLOCK

 10     >>   51 LOAD_FAST                0 (a)
             54 RETURN_VALUE

这很简单。它遍历a,如果它找到值1,则用STORE_SUBSCR将其替换为6。

如果我用这个函数代表理解变体:

def func2(a):     a = [6如果x == 1其他x代表x in a]     返回(a)中

并拆解它,我得到以下内容:

func2:
  7           0 LOAD_CONST               1 (<code object <listcomp> at 0x00000000035731E0, file "<file_path>", line 7>)
              3 LOAD_CONST               2 ('func2.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_FAST                0 (a)
             12 GET_ITER
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             16 STORE_FAST               0 (a)

  8          19 LOAD_FAST                0 (a)
             22 RETURN_VALUE

这比之前的要短。但是它从代码对象加载开始。 func2具有以下代码常量:

>>> func2.__code__.co_consts
(None, <code object <listcomp> at 0x00000000035731E0, file "<file_path>", line 7>, 'func2.<locals>.<listcomp>')

并且listcomp代码对象如下所示:

>>> dis.dis(func2.__code__.co_consts[1].co_code)
          0 BUILD_LIST               0
          3 LOAD_FAST                0 (0)
    >>    6 FOR_ITER                30 (to 39)
          9 STORE_FAST               1 (1)
         12 LOAD_FAST                1 (1)
         15 LOAD_CONST               0 (0)
         18 COMPARE_OP               2 (==)
         21 POP_JUMP_IF_FALSE       30
         24 LOAD_CONST               1 (1)
         27 JUMP_FORWARD             3 (to 33)
    >>   30 LOAD_FAST                1 (1)
    >>   33 LIST_APPEND              2
         36 JUMP_ABSOLUTE            6
    >>   39 RETURN_VALUE

所以基本上这两个实现执行类似的步骤。主要区别在于理解版本用CALL_FUNCTION替换了FOR_ITER。

由此我应该明白为什么列表理解更快,但我不知道。 所以我原来的问题仍然存在:

为什么列表理解更快?

0 个答案:

没有答案