我正在比较any()
内置功能的性能与文档建议的实际实现:
我正在寻找以下列表中大于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()
如果慢了两次。为什么?
原因是您已将A 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)
在这种情况下,您正在与发电机表达式进行比较,并且它几乎在同等的时间内与您的手动定义函数(丝毫不同(根本的原因阅读了Ashwini的答案。
与列表相比,发电机表达式上的循环肯定会较慢。但是在这种情况下,发电机内的迭代基本上是列表本身的循环,因此next()
在生成器上呼叫基本上委派给列表的next()
方法。
例如,在这种情况下没有2x性能差异。
>>> 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)
bytecodes:
>>> 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
检查一次条件的真实值,并基于此返回True
或False
。
现在,让我们添加另一个基于any()
的版本,该版本具有for-loop中的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()
所做的工作(查看最后一部分以获取更多详细信息(,并且在条件为True
时只能检查一次真相,否则它基本上会跳至下一个项目。
现在让我们比较这3:
的时间。>>> %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
条件也将导致两次真实检查,但是当比较操作返回Py_True
或Py_False
时,情况并非如此。POP_JUMP_IF_FALSE
只需跳到下一个OP代码,而没有拨打PyObject_IsTrue
。
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提出的条件子句的生成器表达式甚至更快。
性能的主要部分归结为for
循环。
在您的any
中,有两个用于循环:for elm in lst
和any
进行的FOR循环。因此,在看起来像False, False, False, ..., True
在您的gt_0
中,循环只有一个。
如果将其更改以检查该元素是否完全是真实的,那么它们俩都只循环一次:
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