为什么列表乘法如此快速?

5
创建列表时,尽可能使用理解式推导,因为它是最快的。但是,看起来并非总是如此。
In [1]: %timeit -n1000 [0]*1000000
1000 loops, best of 3: 2.3 ms per loop

In [2]: %timeit -n1000 [0 for _ in range(1000000)]
1000 loops, best of 3: 27.1 ms per loop

In [3]: a = np.zeros(1000000, dtype=int)

In [4]: %timeit -n1000 a.tolist()
1000 loops, best of 3: 7.93 ms per loop

即使是numpy.ndarray.tolist也无法与乘法相匹配。为什么会这样呢?

#2 实际上运行了一个Python循环,而#1则完全没有任何Python循环进行解释。 - spectras
2个回答

5

dis模块对比前两种方法非常有用。

def list_mult():
    return [0]*1000000

dis.dis(list_mult)
#  2           0 LOAD_CONST               1 (0)
#              3 BUILD_LIST               1
#              6 LOAD_CONST               2 (1000000)
#              9 BINARY_MULTIPLY     
#             10 RETURN_VALUE        

这里使用了BINARY_MULTIPLY指令。另一方面...

def list_range():
    return [0 for _ in range(1000000)]

dis.dis(list_range)
# 2           0 BUILD_LIST               0
#             3 LOAD_GLOBAL              0 (range)
#             6 LOAD_CONST               1 (1000000)
#             9 CALL_FUNCTION            1
#            12 GET_ITER            
#       >>   13 FOR_ITER                12 (to 28)
#            16 STORE_FAST               0 (_)
#            19 LOAD_CONST               2 (0)
#            22 LIST_APPEND              2
#            25 JUMP_ABSOLUTE           13
#       >>   28 RETURN_VALUE    

这个函数明确构建了一个循环,并在每次迭代中加载0并将其附加到工作列表中。这会使速度变慢。

需要注意的是,这两种构造方法不等价,特别是当列表内的值是可变的时。例如,[object()] * 10将给出包含10个相同对象的列表,而[object() for _ in range(10)]将给出包含10个不同对象的列表。

关于numpy示例,这个操作对于numpy来说是最糟糕的情况。构造和转换numpy数组的开销很大,以便矢量化操作可以快速进行(如评论中所述)。


1
在第一种情况下,Python 可以创建一个包含所有零的列表,这些零都指向同一个 id。这很好地解决了原始数据类型是字面值的问题,但如果你传递一个对象,它就不能按预期工作了。在这种情况下,每个元素实际上都是对同一个对象的引用。
range 的情况下,会进行函数调用,因此会产生更多的开销。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接