为什么在 Python 中列表理解可以比 map() 更快?

Why list comprehension can be faster than map() in Python?

我正在研究 Python 中类似循环结构的性能问题,发现了以下语句:

Besides the syntactic benefit of list comprehensions, they are often as fast or faster than equivalent use of map. (Performance Tips)

List comprehensions run a bit faster than equivalent for-loops (unless you're just going to throw away the result). (Python Speed)

我想知道引擎盖下的什么区别使列表理解具有这种优势。谢谢

测试一: 扔掉结果。

这是我们的虚拟函数:

def examplefunc(x):
    pass

下面是我们的挑战者:

def listcomp_throwaway():
    [examplefunc(i) for i in range(100)]

def forloop_throwaway():
    for i in range(100):
        examplefunc(i)

我不会对其原始速度进行分析,只是 为什么,根据 OP 的问题。让我们来看看机器码的差异。

--- List comprehension
+++ For loop
@@ -1,15 +1,16 @@
- 55           0 BUILD_LIST               0
+ 59           0 SETUP_LOOP              30 (to 33)
               3 LOAD_GLOBAL              0 (range)
               6 LOAD_CONST               1 (100)
               9 CALL_FUNCTION            1
              12 GET_ITER            
-        >>   13 FOR_ITER                18 (to 34)
+        >>   13 FOR_ITER                16 (to 32)
              16 STORE_FAST               0 (i)
-             19 LOAD_GLOBAL              1 (examplefunc)
+
+ 60          19 LOAD_GLOBAL              1 (examplefunc)
              22 LOAD_FAST                0 (i)
              25 CALL_FUNCTION            1
-             28 LIST_APPEND              2
-             31 JUMP_ABSOLUTE           13
-        >>   34 POP_TOP             
-             35 LOAD_CONST               0 (None)
-             38 RETURN_VALUE        
+             28 POP_TOP             
+             29 JUMP_ABSOLUTE           13
+        >>   32 POP_BLOCK           
+        >>   33 LOAD_CONST               0 (None)
+             36 RETURN_VALUE     

比赛开始了。 Listcomp 的第一步是构建一个空列表,而 for 循环的第一步是设置一个循环。然后它们都继续加载全局 range(),常量 100,并为生成器调用 range 函数。然后他们都得到当前迭代器并得到下一个项目,并将其存储到变量 i 中。然后他们加载 examplefunc 和 i 并调用 examplefunc。 Listcomp 将其附加到列表中并重新开始循环。 For 循环在三个指令而不是两个指令中执行相同的操作。然后他们都加载 None 和 return 它。

那么在这个分析中谁看起来更好?在这里,如果您不关心结果,列表理解会执行一些冗余操作,例如构建列表并附加到列表中。 For 循环也非常有效。

如果你给它们计时,使用 for 循环比列表理解快三分之一。 (在这个测试中,examplefunc 将它的参数除以 5 然后扔掉而不是什么都不做。)

测试二:结果保持正常

本次测试无虚拟功能。下面是我们的挑战者:

def listcomp_normal():
    l = [i*5 for i in range(100)]


def forloop_normal():
    l = []
    for i in range(100):
        l.append(i*5)

差异对我们今天没有任何用处。就是两个block里的两个机器码。

列出 comp 的机器码:

 55           0 BUILD_LIST               0
              3 LOAD_GLOBAL              0 (range)
              6 LOAD_CONST               1 (100)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                16 (to 32)
             16 STORE_FAST               0 (i)
             19 LOAD_FAST                0 (i)
             22 LOAD_CONST               2 (5)
             25 BINARY_MULTIPLY     
             26 LIST_APPEND              2
             29 JUMP_ABSOLUTE           13
        >>   32 STORE_FAST               1 (l)
             35 LOAD_CONST               0 (None)
             38 RETURN_VALUE        

For循环的机器码:

 59           0 BUILD_LIST               0
              3 STORE_FAST               0 (l)

 60           6 SETUP_LOOP              37 (to 46)
              9 LOAD_GLOBAL              0 (range)
             12 LOAD_CONST               1 (100)
             15 CALL_FUNCTION            1
             18 GET_ITER            
        >>   19 FOR_ITER                23 (to 45)
             22 STORE_FAST               1 (i)

 61          25 LOAD_FAST                0 (l)
             28 LOAD_ATTR                1 (append)
             31 LOAD_FAST                1 (i)
             34 LOAD_CONST               2 (5)
             37 BINARY_MULTIPLY     
             38 CALL_FUNCTION            1
             41 POP_TOP             
             42 JUMP_ABSOLUTE           19
        >>   45 POP_BLOCK           
        >>   46 LOAD_CONST               0 (None)
             49 RETURN_VALUE        

您可能已经知道,列表推导式的指令少于 for 循环。

列表理解清单:

  1. 构建一个匿名空列表。
  2. 加载range.
  3. 加载100.
  4. 呼叫range.
  5. 获取迭代器。
  6. 获取该迭代器上的下一项。
  7. 将该项目存储到 i
  8. 加载i.
  9. 加载整数五。
  10. 乘以五。
  11. 附加列表。
  12. 重复步骤 6-10 直到范围为空。
  13. l指向匿名空列表。

For 循环的清单:

  1. 构建一个匿名空列表。
  2. l指向匿名空列表。
  3. 设置循环。
  4. 加载range.
  5. 加载100.
  6. 呼叫range.
  7. 获取迭代器。
  8. 获取该迭代器上的下一项。
  9. 将该项目存储到 i
  10. 加载列表 l
  11. 在该列表中加载属性 append
  12. 加载i.
  13. 加载整数五。
  14. 乘以五。
  15. 呼叫append.
  16. 转到顶部。
  17. 转到绝对。

(不包括这些步骤:加载 None、return 它。)

列表理解不必做这些事情:

  • 每次都加载列表的追加,因为它已预先绑定为局部变量。
  • 每个循环加载 i 两次
  • 花费两条指令到达顶部
  • 直接附加到列表而不是调用附加列表的包装器

总而言之,如果您要使用这些值,listcomp 会快很多,但如果您不使用这些值,它就会很慢。

实际速度

测试一:for循环快三分之一*

测试二:列表理解速度提高约三分之二*

*关于 -> 小数点后第二位 acurrate