为什么在 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 循环。
列表理解清单:
- 构建一个匿名空列表。
- 加载
range
.
- 加载
100
.
- 呼叫
range
.
- 获取迭代器。
- 获取该迭代器上的下一项。
- 将该项目存储到
i
。
- 加载
i
.
- 加载整数五。
- 乘以五。
- 附加列表。
- 重复步骤 6-10 直到范围为空。
- 将
l
指向匿名空列表。
For 循环的清单:
- 构建一个匿名空列表。
- 将
l
指向匿名空列表。
- 设置循环。
- 加载
range
.
- 加载
100
.
- 呼叫
range
.
- 获取迭代器。
- 获取该迭代器上的下一项。
- 将该项目存储到
i
。
- 加载列表
l
。
- 在该列表中加载属性
append
。
- 加载
i
.
- 加载整数五。
- 乘以五。
- 呼叫
append
.
- 转到顶部。
- 转到绝对。
(不包括这些步骤:加载 None
、return 它。)
列表理解不必做这些事情:
- 每次都加载列表的追加,因为它已预先绑定为局部变量。
- 每个循环加载
i
两次
- 花费两条指令到达顶部
- 直接附加到列表而不是调用附加列表的包装器
总而言之,如果您要使用这些值,listcomp 会快很多,但如果您不使用这些值,它就会很慢。
实际速度
测试一:for循环快三分之一*
测试二:列表理解速度提高约三分之二*
*关于 -> 小数点后第二位 acurrate
我正在研究 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 循环。
列表理解清单:
- 构建一个匿名空列表。
- 加载
range
. - 加载
100
. - 呼叫
range
. - 获取迭代器。
- 获取该迭代器上的下一项。
- 将该项目存储到
i
。 - 加载
i
. - 加载整数五。
- 乘以五。
- 附加列表。
- 重复步骤 6-10 直到范围为空。
- 将
l
指向匿名空列表。
For 循环的清单:
- 构建一个匿名空列表。
- 将
l
指向匿名空列表。 - 设置循环。
- 加载
range
. - 加载
100
. - 呼叫
range
. - 获取迭代器。
- 获取该迭代器上的下一项。
- 将该项目存储到
i
。 - 加载列表
l
。 - 在该列表中加载属性
append
。 - 加载
i
. - 加载整数五。
- 乘以五。
- 呼叫
append
. - 转到顶部。
- 转到绝对。
(不包括这些步骤:加载 None
、return 它。)
列表理解不必做这些事情:
- 每次都加载列表的追加,因为它已预先绑定为局部变量。
- 每个循环加载
i
两次 - 花费两条指令到达顶部
- 直接附加到列表而不是调用附加列表的包装器
总而言之,如果您要使用这些值,listcomp 会快很多,但如果您不使用这些值,它就会很慢。
实际速度
测试一:for循环快三分之一*
测试二:列表理解速度提高约三分之二*
*关于 -> 小数点后第二位 acurrate