【问题标题】:Python: any() unexpected performancePython:any() 出乎意料的表现
【发布时间】:2017-12-01 19:54:46
【问题描述】:

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

我正在以下列表中查找大于 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() 如果慢两倍。为什么?

【问题讨论】:

  • 您是否尝试过不以0 开头的更加异构的lst
  • 更等效的版本是:%timeit any(True for elm in lst if elm > 0)
  • any() 的实际实现是在 Python 中还是只是 等效 Python 语法?
  • @Chris_Rands 我认为这只是等效的语法?我希望在 C 或其他语言中实现一个内置函数。
  • @AshwiniChaudhary 与any(elm > 0 for elm in lst) 有何不同?

标签: python python-2.7 performance python-3.x any


【解决方案1】:

原因是您已将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的回答。

【讨论】:

  • in gt_0 OP 仍然在函数中进行比较
  • 我认为您的数据具有误导性。您不能只将%timeit any(elm > 0 for elm in lst)%timeit any(test_list) 进行比较,您还需要考虑构建test_list 所需的时间。这些是我的结果:%timeit test_list = [elm > 0 for elm in lst]; any(test_list); 每个循环输出 52.5 毫秒,而%timeit any(elm > 0 for elm in lst) 报告每个循环 38.4 毫秒。所以生成器表达式其实更好。
  • @dabadaba 这不是我想要说明的重点。当然,创建列表并将其传递给 any 比仅将生成器表达式传递给它要慢。关键是你的时间之间的差异。您正在将列表传递给您的手动函数,而 any 您正在使用生成器表达式。
  • @Kasramvd 哦,所以你的观点基本上是使用 next() 从生成器表达式生成新元素比简单地迭代已经构建的列表更昂贵?
  • @dabadaba 是的。您可以使用以下示例 %timeit sum(i for i in lst)%timeit sum(lst)lst = list(range(100)) 看到差异。
【解决方案2】:

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

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

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() 版本中没有跳转条件,它基本上获取了> 比较的值,然后再次使用PyObject_IsTrue 再次检查其真实值。另一方面,gt_0 会检查一次条件的真值,并据此返回TrueFalse

现在让我们添加另一个基于 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 时,它只需要检查两次真值,否则它将基本上跳到下一项。


现在让我们比较一下这 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 条件也会导致两次真实性检查,但当comparison operation 返回Py_TruePy_False 时情况并非如此。 POP_JUMP_IF_FALSE 只是跳转到下一个 OP 代码,不会调用 PyObject_IsTrue

【讨论】:

  • 我不认为这里是否有额外的比较,因为我们所能看到的是,我们都在执行一个比较,并且any 都遵循它的常规程序。似乎差异在于python评估这种比较的方式(在python中和/或将其委托给内置函数)。当我们将额外的condition = elm > 0 添加到手动函数时,它将在 Python 层中执行,而不是在生成器表达式中的编译代码对象中。我认为当我们将 elm > 0 传递给 any 而不是 bool 对象时会发生这种情况。
  • @Kasramvd 我没有说额外的比较,只是elm > 0首先被转换为布尔值,然后any()每次都再次检查其真实性。
  • 对不起,这就是我解释您的答案的方式。因为在第二种情况下,如果我们在生成器表达式中进行比较,仍然会创建一个布尔值并再次检查真实性。我认为我们在第二种情况的字节码中看到额外的POP_JUMP_IF_FALSE 的原因是因为any 遇到的是一个布尔对象而不是比较,并且在这两种情况下操作的数量是相同的,似乎不同之处在于 python 评估该比较的方式。
  • @Kasramvd 如果any() 中有一个if condition 并且输入了一个虚假值,那么True 甚至不会出现,它仅在if conditionTrue 并且在 any() 的情况下只会出现一次,因为它会在第一个真实值之后短路。(这与任何有关(如果 elm > 0,elm 在 lst 中为真))。因此,我不确定您为什么认为会有相同数量的操作。
  • 其次,POP_JUMP_IF_FALSE 在收到Py_TruePy_False(在这种情况下将由比较提供)时简单地跳转到下一个操作码,在这种情况下不会调用PyObject_IsTrue
【解决方案3】:

性能的主要部分归结为for 循环。

在您的any 中,有两个for 循环:for elm in lstany 执行的for 循环。所以,任何迭代一个看起来像 False, False, False, ..., True

的生成器

在您的gt_0 中,只有一个 for 循环。

如果你改变它来检查元素是否真实,那么它们都只循环一次:

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

【讨论】:

  • 我不明白你所说的两个for循环。
【解决方案4】:
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 提出的带有条件子句的生成器表达式甚至更快。

【讨论】:

  • 我没有得到这些结果。我得到 17.4 毫秒、39.1 毫秒和 52.4 毫秒。列表推导式最慢是有道理的,因为它需要构建一个全新的列表,而生成器表达式更快,并且在满足条件时也会停止。在这里,最后一件事没有太大影响,因为我们知道直到最后一个元素才满足条件,但是请注意我是否将 1 移动到列表的开头:每个循环 47 毫秒,列表理解和 430 ns带有生成器表达式。是的,纳秒。巨大的差异。
猜你喜欢
  • 1970-01-01
  • 2014-09-11
  • 2013-07-15
  • 1970-01-01
  • 1970-01-01
  • 2014-09-19
  • 2018-07-14
  • 2015-03-02
相关资源
最近更新 更多