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

Bin*_*Bin 3 python performance interpreter list-comprehension

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

除了列表推导的句法益处之外,它们通常比等效使用map更快或更快.(性能提示)

列表推导比等效的for循环运行得快一点(除非你只是扔掉结果).(Python速度)

我想知道引擎盖下有什么区别可以让列表理解这个优势.谢谢.

noɥ*_*ɐɹƆ 8

测试一:扔掉结果.

这是我们的虚拟功能:

def examplefunc(x):
    pass
Run Code Online (Sandbox Code Playgroud)

以下是我们的挑战者:

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

def forloop_throwaway():
    for i in range(100):
        examplefunc(i)
Run Code Online (Sandbox Code Playgroud)

根据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     
Run Code Online (Sandbox Code Playgroud)

比赛开始了.Listcomp的第一步是建立一个空列表,而for循环是建立一个循环.然后它们都继续加载全局范围(),常量100,并调用生成器的范围函数.然后他们都获得当前的迭代器并获得下一个项目,并将其存储到变量i中.然后他们加载examplefunc和i并调用examplefunc.Listcomp将它附加到列表并再次开始循环.For循环在三个指令中执行相同而不是两个.然后他们都加载None并返回它.

那么谁在这个分析中似乎更好?在这里,列表理解会执行一些冗余操作,例如构建列表并附加到它,如果您不关心结果.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)
Run Code Online (Sandbox Code Playgroud)

差异对我们今天没用.它只是两个块中的两个机器码.

列出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        
Run Code Online (Sandbox Code Playgroud)

对于循环的机器代码:

 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        
Run Code Online (Sandbox Code Playgroud)

正如您可能已经知道的那样,列表理解的指令少于循环.

列表理解的清单:

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

对于循环的核对清单:

  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,返回.)

列表理解不必执行以下操作:

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

总而言之,如果要使用这些值,listcomp会快得多,但如果不这样做,则速度非常慢.

真正的速度

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

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

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