Python: any() 意外的表现
Python: any() unexpected performance
我正在将 any()
内置函数的性能与 docs 建议的实际实现进行比较:
我要在以下列表中查找大于 0 的元素:
lst = [0 for _ in range(1000000)] + [1]
这是假定的等效函数:
def gt_0(lst):
for elm in lst:
if elm > 0:
return True
return False
这些是性能测试的结果:
>> %timeit any(elm > 0 for elm in lst)
>> 10 loops, best of 3: 35.9 ms per loop
>> %timeit gt_0(lst)
>> 100 loops, best of 3: 16 ms per loop
我希望两者具有完全相同的性能,但是 any()
如果慢两倍。为什么?
原因是您已将 generator expression 传递给 any()
函数。 Python 需要将生成器表达式转换为生成器函数,这就是它执行速度较慢的原因。因为生成器函数需要每次调用 __next__()
方法来生成项目并将其传递给 any
。这是在手动定义的函数中,您将整个列表传递给已经准备好所有项目的函数。
通过使用列表理解而不是生成器表达式,您可以更好地看出差异:
In [4]: %timeit any(elm > 0 for elm in lst)
10 loops, best of 3: 66.8 ms per loop
In [6]: test_list = [elm > 0 for elm in lst]
In [7]: %timeit any(test_list)
100 loops, best of 3: 4.93 ms per loop
代码中的另一个瓶颈比 next
上的额外调用成本更高,这是您进行比较的方式。如评论中所述,您的手动功能的更好等价物是:
any(True for elm in lst if elm > 0)
在这种情况下,您正在与生成器表达式进行比较,它的执行时间几乎与您手动定义的函数相同(我猜,最细微的差别是因为生成器。)为了更深入了解根本原因阅读 的回答。
性能的主要部分归结为 for
循环。
在您的 any
中,有两个 for 循环:for elm in lst
和由 any
执行的 for 循环。因此,any 遍历看起来像 False, False, False, ..., True
的生成器
在你的gt_0
中,只有一个for循环。
如果你改变它来检查元素是否完全真实,那么它们都只循环一次:
def _any(lst):
for elm in lst:
if elm:
return True
return False
_any(lst)
any(lst)
有一个明显的赢家:
$ python2 -m timeit "from test import lst, _any" "any(lst)"
100 loops, best of 3: 5.68 msec per loop
$ python2 -m timeit "from test import lst, _any" "_any(lst)"
10 loops, best of 3: 17 msec per loop
print(timeit('any(True for elm in lst if elm > 0)',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
print(timeit('any([elm > 0 for elm in lst])',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
print(timeit('any(elm > 0 for elm in lst)',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
产生:
2.1382904349993623
3.1172365920028824
4.580027656000311
正如 Kasramvd 所解释的,最后一个版本是最慢的,因为它使用了生成器表达式;列表推导式要快一些,但令人惊讶的是,使用 Ashwini Chaudhary 提出的带有条件子句的生成器表达式甚至更快。
与列表相比,生成器表达式的循环肯定更慢。但在这种情况下,生成器中的迭代基本上是对列表本身的循环,因此对生成器的 next()
调用基本上委托给列表的 next()
方法。
例如,在这种情况下,没有 2 倍的性能差异。
>>> lst = list(range(10**5))
>>> %%timeit
... sum(x for x in lst)
...
100 loops, best of 3: 6.39 ms per loop
>>> %%timeit
... c = 0
... for x in lst: c += x
...
100 loops, best of 3: 6.69 ms per loop
首先让我们检查一下两种方法的字节码:
def gt_0(lst):
for elm in lst:
if elm > 0:
return True
return False
def any_with_ge(lst):
return any(elm > 0 for elm in lst)
字节码:
>>> dis.dis(gt_0)
10 0 SETUP_LOOP 30 (to 33)
3 LOAD_FAST 0 (lst)
6 GET_ITER
>> 7 FOR_ITER 22 (to 32)
10 STORE_FAST 1 (elm)
11 13 LOAD_FAST 1 (elm)
16 LOAD_CONST 1 (0)
19 COMPARE_OP 4 (>)
22 POP_JUMP_IF_FALSE 7
12 25 LOAD_GLOBAL 0 (True)
28 RETURN_VALUE
29 JUMP_ABSOLUTE 7
>> 32 POP_BLOCK
13 >> 33 LOAD_GLOBAL 1 (False)
36 RETURN_VALUE
>>> dis.dis(any_with_ge.func_code.co_consts[1])
17 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (elm)
9 LOAD_FAST 1 (elm)
12 LOAD_CONST 0 (0)
15 COMPARE_OP 4 (>)
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 1 (None)
26 RETURN_VALUE
如你所见,any()
版本中没有跳转条件,它基本上是获取 >
比较的值,然后再次使用 PyObject_IsTrue
检查其真值。另一方面,gt_0
检查一次条件的真值,returns True
或 False
以此为基础。
现在让我们添加另一个基于 any()
的版本,它有一个类似 for 循环的 if 条件。
def any_with_ge_and_condition(lst):
return any(True for elm in lst if elm > 0)
字节码:
>>> dis.dis(any_with_ge_and_condition.func_code.co_consts[1])
21 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 23 (to 29)
6 STORE_FAST 1 (elm)
9 LOAD_FAST 1 (elm)
12 LOAD_CONST 0 (0)
15 COMPARE_OP 4 (>)
18 POP_JUMP_IF_FALSE 3
21 LOAD_GLOBAL 0 (True)
24 YIELD_VALUE
25 POP_TOP
26 JUMP_ABSOLUTE 3
>> 29 LOAD_CONST 1 (None)
32 RETURN_VALUE
现在我们通过添加条件减少了 any()
所做的工作(查看最后一节了解更多详细信息)并且当条件为 [=25 时它只需要检查两次 truthy 一次=], 否则它基本上会跳到下一项。
现在让我们比较一下这三个的时间:
>>> %timeit gt_0(lst)
10 loops, best of 3: 26.1 ms per loop
>>> %timeit any_with_ge(lst)
10 loops, best of 3: 57.7 ms per loop
>>> %timeit any_with_ge_and_condition(lst)
10 loops, best of 3: 26.8 ms per loop
让我们修改 gt_0
以包括两个检查,就像在简单 any()
版本中一样,并检查其时间。
from operator import truth
# This calls `PyObject_IsTrue` internally
# https://github.com/python/cpython/blob/master/Modules/_operator.c#L30
def gt_0_truth(lst, truth=truth): # truth=truth to prevent global lookups
for elm in lst:
condition = elm > 0
if truth(condition):
return True
return False
时间:
>>> %timeit gt_0_truth(lst)
10 loops, best of 3: 56.6 ms per loop
现在,让我们看看当我们尝试使用 operator.truth
.
两次检查一个项目的真值时会发生什么
>> %%timeit t=truth
... [t(i) for i in xrange(10**5)]
...
100 loops, best of 3: 5.45 ms per loop
>>> %%timeit t=truth
[t(t(i)) for i in xrange(10**5)]
...
100 loops, best of 3: 9.06 ms per loop
>>> %%timeit t=truth
[t(i) for i in xrange(10**6)]
...
10 loops, best of 3: 58.8 ms per loop
>>> %%timeit t=truth
[t(t(i)) for i in xrange(10**6)]
...
10 loops, best of 3: 87.8 ms per loop
即使我们只是在一个已经是布尔值的对象上简单地调用 truth()
(即 PyObject_IsTrue
),这还是有很大的不同,我想这可以解释基本 any()
版本的缓慢.
您可能会争辩说 any()
中的 if
条件也会导致两次真实性检查,但是 comparison operation returns Py_True
or Py_False
. POP_JUMP_IF_FALSE
simply jumps to the next OP code and no call to PyObject_IsTrue
时情况并非如此。
我正在将 any()
内置函数的性能与 docs 建议的实际实现进行比较:
我要在以下列表中查找大于 0 的元素:
lst = [0 for _ in range(1000000)] + [1]
这是假定的等效函数:
def gt_0(lst):
for elm in lst:
if elm > 0:
return True
return False
这些是性能测试的结果:
>> %timeit any(elm > 0 for elm in lst)
>> 10 loops, best of 3: 35.9 ms per loop
>> %timeit gt_0(lst)
>> 100 loops, best of 3: 16 ms per loop
我希望两者具有完全相同的性能,但是 any()
如果慢两倍。为什么?
原因是您已将 generator expression 传递给 any()
函数。 Python 需要将生成器表达式转换为生成器函数,这就是它执行速度较慢的原因。因为生成器函数需要每次调用 __next__()
方法来生成项目并将其传递给 any
。这是在手动定义的函数中,您将整个列表传递给已经准备好所有项目的函数。
通过使用列表理解而不是生成器表达式,您可以更好地看出差异:
In [4]: %timeit any(elm > 0 for elm in lst)
10 loops, best of 3: 66.8 ms per loop
In [6]: test_list = [elm > 0 for elm in lst]
In [7]: %timeit any(test_list)
100 loops, best of 3: 4.93 ms per loop
代码中的另一个瓶颈比 next
上的额外调用成本更高,这是您进行比较的方式。如评论中所述,您的手动功能的更好等价物是:
any(True for elm in lst if elm > 0)
在这种情况下,您正在与生成器表达式进行比较,它的执行时间几乎与您手动定义的函数相同(我猜,最细微的差别是因为生成器。)为了更深入了解根本原因阅读
性能的主要部分归结为 for
循环。
在您的 any
中,有两个 for 循环:for elm in lst
和由 any
执行的 for 循环。因此,any 遍历看起来像 False, False, False, ..., True
在你的gt_0
中,只有一个for循环。
如果你改变它来检查元素是否完全真实,那么它们都只循环一次:
def _any(lst):
for elm in lst:
if elm:
return True
return False
_any(lst)
any(lst)
有一个明显的赢家:
$ python2 -m timeit "from test import lst, _any" "any(lst)"
100 loops, best of 3: 5.68 msec per loop
$ python2 -m timeit "from test import lst, _any" "_any(lst)"
10 loops, best of 3: 17 msec per loop
print(timeit('any(True for elm in lst if elm > 0)',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
print(timeit('any([elm > 0 for elm in lst])',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
print(timeit('any(elm > 0 for elm in lst)',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
产生:
2.1382904349993623
3.1172365920028824
4.580027656000311
正如 Kasramvd 所解释的,最后一个版本是最慢的,因为它使用了生成器表达式;列表推导式要快一些,但令人惊讶的是,使用 Ashwini Chaudhary 提出的带有条件子句的生成器表达式甚至更快。
与列表相比,生成器表达式的循环肯定更慢。但在这种情况下,生成器中的迭代基本上是对列表本身的循环,因此对生成器的 next()
调用基本上委托给列表的 next()
方法。
例如,在这种情况下,没有 2 倍的性能差异。
>>> lst = list(range(10**5))
>>> %%timeit
... sum(x for x in lst)
...
100 loops, best of 3: 6.39 ms per loop
>>> %%timeit
... c = 0
... for x in lst: c += x
...
100 loops, best of 3: 6.69 ms per loop
首先让我们检查一下两种方法的字节码:
def gt_0(lst):
for elm in lst:
if elm > 0:
return True
return False
def any_with_ge(lst):
return any(elm > 0 for elm in lst)
字节码:
>>> dis.dis(gt_0)
10 0 SETUP_LOOP 30 (to 33)
3 LOAD_FAST 0 (lst)
6 GET_ITER
>> 7 FOR_ITER 22 (to 32)
10 STORE_FAST 1 (elm)
11 13 LOAD_FAST 1 (elm)
16 LOAD_CONST 1 (0)
19 COMPARE_OP 4 (>)
22 POP_JUMP_IF_FALSE 7
12 25 LOAD_GLOBAL 0 (True)
28 RETURN_VALUE
29 JUMP_ABSOLUTE 7
>> 32 POP_BLOCK
13 >> 33 LOAD_GLOBAL 1 (False)
36 RETURN_VALUE
>>> dis.dis(any_with_ge.func_code.co_consts[1])
17 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (elm)
9 LOAD_FAST 1 (elm)
12 LOAD_CONST 0 (0)
15 COMPARE_OP 4 (>)
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 1 (None)
26 RETURN_VALUE
如你所见,any()
版本中没有跳转条件,它基本上是获取 >
比较的值,然后再次使用 PyObject_IsTrue
检查其真值。另一方面,gt_0
检查一次条件的真值,returns True
或 False
以此为基础。
现在让我们添加另一个基于 any()
的版本,它有一个类似 for 循环的 if 条件。
def any_with_ge_and_condition(lst):
return any(True for elm in lst if elm > 0)
字节码:
>>> dis.dis(any_with_ge_and_condition.func_code.co_consts[1])
21 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 23 (to 29)
6 STORE_FAST 1 (elm)
9 LOAD_FAST 1 (elm)
12 LOAD_CONST 0 (0)
15 COMPARE_OP 4 (>)
18 POP_JUMP_IF_FALSE 3
21 LOAD_GLOBAL 0 (True)
24 YIELD_VALUE
25 POP_TOP
26 JUMP_ABSOLUTE 3
>> 29 LOAD_CONST 1 (None)
32 RETURN_VALUE
现在我们通过添加条件减少了 any()
所做的工作(查看最后一节了解更多详细信息)并且当条件为 [=25 时它只需要检查两次 truthy 一次=], 否则它基本上会跳到下一项。
现在让我们比较一下这三个的时间:
>>> %timeit gt_0(lst)
10 loops, best of 3: 26.1 ms per loop
>>> %timeit any_with_ge(lst)
10 loops, best of 3: 57.7 ms per loop
>>> %timeit any_with_ge_and_condition(lst)
10 loops, best of 3: 26.8 ms per loop
让我们修改 gt_0
以包括两个检查,就像在简单 any()
版本中一样,并检查其时间。
from operator import truth
# This calls `PyObject_IsTrue` internally
# https://github.com/python/cpython/blob/master/Modules/_operator.c#L30
def gt_0_truth(lst, truth=truth): # truth=truth to prevent global lookups
for elm in lst:
condition = elm > 0
if truth(condition):
return True
return False
时间:
>>> %timeit gt_0_truth(lst)
10 loops, best of 3: 56.6 ms per loop
现在,让我们看看当我们尝试使用 operator.truth
.
>> %%timeit t=truth
... [t(i) for i in xrange(10**5)]
...
100 loops, best of 3: 5.45 ms per loop
>>> %%timeit t=truth
[t(t(i)) for i in xrange(10**5)]
...
100 loops, best of 3: 9.06 ms per loop
>>> %%timeit t=truth
[t(i) for i in xrange(10**6)]
...
10 loops, best of 3: 58.8 ms per loop
>>> %%timeit t=truth
[t(t(i)) for i in xrange(10**6)]
...
10 loops, best of 3: 87.8 ms per loop
即使我们只是在一个已经是布尔值的对象上简单地调用 truth()
(即 PyObject_IsTrue
),这还是有很大的不同,我想这可以解释基本 any()
版本的缓慢.
您可能会争辩说 any()
中的 if
条件也会导致两次真实性检查,但是 comparison operation returns Py_True
or Py_False
. POP_JUMP_IF_FALSE
simply jumps to the next OP code and no call to PyObject_IsTrue
时情况并非如此。