Python:任何()意外的性能

dab*_*aba 5 python performance any python-2.7 python-3.x

我将any()内置函数的性能与文档建议的实际实现进行比较:

我在下面的列表中寻找一个大于0的元素:

lst = [0 for _ in range(1000000)] + [1]
Run Code Online (Sandbox Code Playgroud)

这是所谓的等效功能:

def gt_0(lst):
    for elm in lst:
        if elm > 0:
            return True
    return False
Run Code Online (Sandbox Code Playgroud)

这些是性能测试的结果:

>> %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
Run Code Online (Sandbox Code Playgroud)

我希望两者具有完全相同的性能,但是any()如果慢两倍.为什么?

Ash*_*ary 7

与列表相比,生成器表达式上的循环肯定更慢。但在这种情况下,生成器内的迭代基本上是对列表本身的循环,因此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
Run Code Online (Sandbox Code Playgroud)

首先让我们检查两种方法的字节码:

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)
Run Code Online (Sandbox Code Playgroud)

字节码:

>>> 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
Run Code Online (Sandbox Code Playgroud)

如您所见,any()版本中没有跳转条件,它基本上获取>比较的值,然后再次使用PyObject_IsTrue再次检查其真值。另一方面,gt_0检查条件的真值一次并返回TrueFalse基于此返回。

现在让我们添加另一个any()具有 if 条件的版本,如 for 循环。

def any_with_ge_and_condition(lst):
    return any(True for elm in lst if elm > 0)
Run Code Online (Sandbox Code Playgroud)

字节码:

>>> 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
Run Code Online (Sandbox Code Playgroud)

现在我们any()通过添加条件来减少工作量(查看上一节了解更多详细信息),当条件变为 时,它只需要检查一次真值两次True,否则它基本上会跳到下一项。


现在让我们比较一下这三个的时间:

>>> %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
Run Code Online (Sandbox Code Playgroud)

让我们修改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
Run Code Online (Sandbox Code Playgroud)

定时:

>>> %timeit gt_0_truth(lst)
10 loops, best of 3: 56.6 ms per loop
Run Code Online (Sandbox Code Playgroud)

现在,让我们看看当我们尝试使用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
Run Code Online (Sandbox Code Playgroud)

即使我们只是在一个已经是布尔值的对象上调用truth()(即PyObject_IsTrue),这也是一个很大的不同,我想这可以解释基本any()版本的缓慢。


您可能会争辩说,if条件 inany()也会导致两次真实性检查,但当比较操作返回Py_Trueor时情况并非如此Py_FalsePOP_JUMP_IF_FALSE简单地跳转到下一个 OP 代码并且不调用PyObject_IsTrue


Kas*_*mvd 5

原因是您已将生成器表达式传递给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
Run Code Online (Sandbox Code Playgroud)

您的代码中的另一个瓶颈是比next您进行比较的方式更多的成本而不是额外的调用.正如评论中所提到的,更好的手动功能相当于:

any(True for elm in lst if elm > 0)
Run Code Online (Sandbox Code Playgroud)

在这种情况下,你正在与生成器表达式进行比较,并且它几乎会在与手动定义的函数相同的时间内执行(最轻微的区别是由于生成器,我猜.)为了更深入地理解基本原因阅读Ashwini的回答.