Python:any()意外性能
我将Python:any()意外性能,python,python-2.7,performance,python-3.x,any,Python,Python 2.7,Performance,Python 3.x,Any,我将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
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传递给any()
函数。Python需要将生成器表达式转换为生成器函数,这就是它执行速度较慢的原因。因为生成器函数每次都需要调用\uuuu next\uuu()
方法来生成项并将其传递给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循环:用于lst中的elm
和由any
执行的for循环。因此,任何对生成器的迭代看起来像False,False,False,…,True
在gt_0
中,只有一个for循环
如果将其更改为检查元素是否真实,则它们都只循环一次:
def _any(lst):
for elm in lst:
if elm:
return True
return False
_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
产生:
2.1382904349993623
3.1172365920028824
4.580027656000311
正如Kasramvd所解释的,最后一个版本速度最慢,因为它使用的是生成器表达式;列表理解要快一点,但令人惊讶的是,使用Ashwini Chaudhary提出的带条件子句的生成器表达式更快。当然,生成器表达式上的循环比列表上的循环要慢。但在这种情况下,生成器中的迭代基本上是列表本身的循环,因此对生成器的
next()
调用基本上委托给list的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()
版本中没有跳转条件,它基本上获得了
比较的值,然后再次使用检查其真实值。另一方面,gt_0
检查条件的真实值一次,并基于此返回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()
所做的工作(查看最后一节了解更多详细信息),当条件为True
时,它只需检查truthy两次,否则基本上会跳到下一项
现在,让我们比较一下这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
现在,让我们看看当我们使用
操作符.truth
两次检查项目的truthy值时会发生什么
>> %%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()
(即),我想这可以解释basicany()
版本的缓慢性
您可能认为
any()
中的if
条件也会导致两次真实性检查,但当返回Py\u True
或Py\u False
时,情况并非如此。只需跳转到下一个操作代码,就不会调用。您是否尝试过使用更异构的lst
而不是以0
开头?更等效的版本应该是:%timeit any(如果elm>0,则对lst中的elm为真)
。还有any()的实际实现
是用Python编写的,还是这只是等效的Python语法?@Chris_Rands我想这只是等效的语法?我希望一个内置函数可以用C或其他语言实现。@AshwiniChaudhary这与any(elm>0表示lst中的elm)
?在gt_0
中,OP仍然在函数中进行比较,尽管我认为您的数据有误导性。不能只比较<代码> %TimeIt(LSM的ELM > 0)<代码> > <代码> %Times(TestILLIST),还需要考虑建立<代码> TestOpList所需的时间。以下是我的结果:%timeit test_list=[elm>0表示lst中的elm];任何(测试列表)
每个循环输出52.5毫秒,而%timeit any(对于lst中的elm,elm>0)
报告每个循环38.4毫秒。所以生成器表达式实际上更好。@dabadaba这不是我想说的重点。当然,创建列表并将其传递给any要比仅将生成器表达式传递给它慢。关键是你的时间安排不同。您正在将列表传递给手动函数,而对于any
您正在使用生成器表达式。@Kasramvd噢,您的观点基本上是,使用next()
从生成器表达式生成新元素要比简单地迭代已构建的列表花费更高?@dabadaba Yes。您可以使用以下示例查看差异%timeit sum(i表示lst中的i)
>>> %timeit gt_0_truth(lst)
10 loops, best of 3: 56.6 ms per loop
>> %%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