Python:任何()意外的性能



我正在比较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检查一次条件的真实值,并基于此返回TrueFalse

现在,让我们添加另一个基于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_TruePy_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 lstany进行的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

相关内容

  • 没有找到相关文章

最新更新