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()
(即),我想这可以解释basic
any()
版本的缓慢性



您可能认为
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