【问题标题】:Why is any (True for ... if cond) much faster than any (cond for ...)?为什么 any (True for ... if cond) 比 any (cond for ...) 快得多?
【发布时间】:2021-10-26 12:30:11
【问题描述】:

检查列表是否包含奇数的两种类似方法:

any(x % 2 for x in a)
any(True for x in a if x % 2)

a = [0] * 10000000 的计时结果(每次尝试五次,时间以秒为单位):

0.60  0.60  0.60  0.61  0.63  any(x % 2 for x in a)
0.36  0.36  0.36  0.37  0.37  any(True for x in a if x % 2)

为什么第二种方式几乎快两倍?

我的测试代码:

from timeit import repeat

setup = 'a = [0] * 10000000'

expressions = [
    'any(x % 2 for x in a)',
    'any(True for x in a if x % 2)',
]

for expression in expressions:
    times = sorted(repeat(expression, setup, number=1))
    print(*('%.2f ' % t for t in times), expression)

Try it online!

【问题讨论】:

  • 查看过滤方法的另一种方式是next((True for x in a if x % 2), False),它使用显式默认值来明确过滤生成器不会产生任何错误值,即使仅a包含偶数。

标签: python performance


【解决方案1】:

第一种方法将所有内容发送到any(),而第二种方法仅在有奇数时发送到any(),因此any() 需要经过的元素更少。

【讨论】:

  • 其他答案有更多细节,但我认为这个答案的简洁明了使它成为第一个阅读的最佳答案。
  • 需要注意的是,既然是生成器,“一切”并不一定意味着它处理了a的所有元素。两个版本在到达第一个奇数时都会停止。但是在测试用例中,没有奇数,所以生成器确实要遍历整个列表。
【解决方案2】:
(x % 2 for x in a)

这个生成器会产生一系列虚假值,直到它产生一个真实值(如果有的话),此时any 将停止迭代生成器并返回True

(True for x in a if x % 2)

这个生成器只会产生一个True 值(如果有的话),此时any 将停止迭代并返回True

在第一种情况下,返回到 any 然后从生成器中获取下一个值的额外来回导致了开销。

【讨论】:

  • 在给定的示例中,所有数字都是偶数,因此不会产生 True 值。
  • 那么第二种方式总是更快还是同样快?那么一般情况下更可取吗?到目前为止,我一直在使用第一种方式。
  • @don't 这对我来说看起来像是一个微优化,所以你应该只在它真的对你产生影响的情况下才去这样做,因为它在实际实践中显着加快了你的算法。我不会因为……而默认这种风格。
  • @deceze 我必须同意,这是一个严重的微优化案例。我们谈论的是在 10M 个元素的列表上运行 any() 大约需要 30ns 的额外时间。这个问题和答案仍然很有趣,但我会坚持使用更易读的版本,即使它稍微慢一点。对于更快的版本,我觉得有必要写一个评论,说明我为什么要使用如此令人惊讶的代码行。
【解决方案3】:

TL;DR 慢速版本必须在返回 False 之前迭代一长串错误值。快速版本在执行相同操作之前会“迭代”一个空序列。区别在于构造 long-false 序列与空序列所需的时间。


让我们看看每个生成的字节码。我已经省略了每个的第一部分,因为它们对于两者都是相同的。我们需要查看的只是所涉及的生成器的代码。

In [5]: dis.dis('any(x%2 for x in a)')
[...]

Disassembly of <code object <genexpr> at 0x105e860e0, file "<dis>", line 1>:
  1           0 LOAD_FAST                0 (.0)
        >>    2 FOR_ITER                14 (to 18)
              4 STORE_FAST               1 (x)
              6 LOAD_FAST                1 (x)
              8 LOAD_CONST               0 (2)
             10 BINARY_MODULO
             12 YIELD_VALUE
             14 POP_TOP
             16 JUMP_ABSOLUTE            2
        >>   18 LOAD_CONST               1 (None)
             20 RETURN_VALUE


In [6]: dis.dis('any(True for x in a if x % 2)')
[...]

Disassembly of <code object <genexpr> at 0x105d993a0, file "<dis>", line 1>:
  1           0 LOAD_FAST                0 (.0)
        >>    2 FOR_ITER                18 (to 22)
              4 STORE_FAST               1 (x)
              6 LOAD_FAST                1 (x)
              8 LOAD_CONST               0 (2)
             10 BINARY_MODULO
             12 POP_JUMP_IF_FALSE        2
             14 LOAD_CONST               1 (True)
             16 YIELD_VALUE
             18 POP_TOP
             20 JUMP_ABSOLUTE            2
        >>   22 LOAD_CONST               2 (None)
             24 RETURN_VALUE

两者都与BINARY_MODULO 指令相同。之后,较慢的版本必须产生结果值以供any 使用,然后再继续,而第二个代码立即 移动到下一个值。所以基本上,较慢的代码必须消耗一长串假(即非零)值来确定没有真值。更快的代码只需要消耗一个空列表。

【讨论】:

    【解决方案4】:

    前面的答案在某种程度上假设读者已经熟悉语法和生成器。我想为不知道的人解释更多。

    sn-p

    any(x % 2 for x in a)
    

    是以下的简短语法:

    any((x % 2 for x in a))
    

    所以发生的事情是 (x % 2 for x in a) 被评估,然后将结果值提供给 any 函数。就像 print(21 * 2) 计算值 42,然后将其提供给 print 函数。

    表达式(x % 2 for x in a)generator expression,其结果是生成器迭代器。这是一个根据需要计算和分发其值的对象。所以在这种情况下,当被要求输入一个值时,这个迭代器会查看来自a 的下一个值,计算其余数模 2(即,0 表示偶数,1 表示奇数),然后分发该值。然后从字面上 等待 可能再次被要求提供另一个值。

    any 函数在这里是第二个演员。它获取迭代器作为参数,然后向迭代器询问越来越多的值,希望得到一个正确的值(注意 1 为真,0 为假)。

    你真的可以把它想象成两个不同的人在互动。任何人向迭代器人询问值。再次注意,迭代器确实提前计算所有值。每次只有一个,每当任何人要求下一个值时。所以这真的是两个人之间的来回。

    any(x % 2 for x in a) 的情况下,iterator-guy 每当被 any-guy 询问下一个值时,只计算一个模值,将其交给 any-guy,然后 any-guy 必须判断它。在这里,迭代器人就像一个不称职的初级开发人员,每个数字都涉及经理,在某种程度上迫使他们进行核心的微观管理。

    any(True for x in a if x % 2) 的情况下,迭代器人,无论何时被任何人询问下一个值,都不会盲目地只交出模值。取而代之的是,这个迭代器人自己判断值,并且只有在有值得交出的东西时才交出东西给经理。即仅当他发现奇数时(在这种情况下,他不会交出01,而是交出True)。在这里,迭代者就像一个称职的高级开发人员,负责所有的工作,而经理则可以完全放松身心(归根结底,他们仍然承担所有功劳)。

    应该清楚的是,第二种方式效率更高,因为它们不需要为每个 ... 单个 ... 输入数字进行通信。特别是因为您的输入 a = [0] * 10000000 不包含 任何 奇数。初级开发人员向必须判断所有零的经理报告一千万个零。对于每个零,它们之间都有一个不断的来回。高级开发人员自己判断一切并向他的经理报告任何内容。好吧,好的,最后两个开发人员还报告说他们已经完成了,此时经理得出结论 False 作为整个 any(...) 表达式的结果。

    【讨论】:

    • 我已阅读所有三个答案。这是最令人困惑的一个,我仍然不知道是什么导致了所讨论的两个表达式之间的性能差异。 True for x in a if x % 2 不正是 any(x % 2 for x in a) 所做的吗?
    • @oguzismail True for x in a if x % 2 甚至不是一个有效的表达方式,你能澄清一下你的意思吗?
    • 附在any(...)中,就是我说的意思。我已经告诉你我真正的意思,而你拒绝理解。因此投反对票。祝你有美好的一天。
    • @oguzismail 我不想知道您所说的代码的“含义”,我想知道您的意思是什么code。因为我不能讨论我不知道的代码。所以你的意思是any(True for x in a if x % 2)?是的,any(x % 2 for x in a)any(True for x in a if x % 2) 具有相同的含义,几乎就是您所说的“如果 x % 2 对于 a 中的任何元素为真,则返回真”。但他们以不同的方式实现这一目标,其中一种更快。就像 min(a)sorted(a)[0] 表示相同的东西(“a 的最小值”),但以不同的方式计算,速度也不同。
    • 是的。您所说的与使用更多单词的最高投票答案相同。谢谢
    【解决方案5】:

    “检查虚假”的数量并不是真正的原因,因为在更快的版本中,我们可以看到实习生调用 bool()if 语句。在更快的情况下,检查是“提前”完成的。因此,在这两种情况下,python 都必须检查 所有值 并检查 所有值 的真实性。

    Chepner的答案中显示的过程确实是问题的答案。让我们看看什么时候可以请求 for 循环中的下一项...:

    在更快的情况下,它就在BINARY_MODULO之后,但是POP_JUMP_IF_FALSE语句中它必须做一些工作来检查真实性if 调用 bool()) 而在较慢的版本中它不会在那里检查。到目前为止 (-1) 点以获得更快的版本。但在较慢的版本中,它必须执行三个步骤才能到达请求下一项的点,YIELD_VALUEPOP_TOPJUMP_ABSOLUTE。所以(-3)对于较慢的版本......这三个步骤会导致开销,因为它们不能被跳过。

    换句话说,更快的版本只做“检查”以达到要求下一个项目的点,而较慢的版本必须做“检查+那些步骤”。他们再次检查所有值的真实性。

    答案是屈服的开销。

    【讨论】:

    • 小吹牛:if 不会调用 bool()
    • @don'ttalkjustcode 确实如此,请检查this link
    • 不,那是对象的__bool__ 方法。声明 'bool is calling' 不正确。别的东西叫它。
    • @don'ttalkjustcode 因为这将是题外话,对于最后一个问题,我说if obj: 声明与if bool(obj) is True: 所做的完全一样,但是你拒绝它并且你说Some else calls __bool__。你能告诉我这是如何工作的吗? if obj: 还检查从 obj__bool__ 方法返回的布尔值。
    • 我认为它必须是 tp_as_number-&gt;nb_bool 选项,也许 as_number 只是一个误导性名称。
    猜你喜欢
    • 2022-06-12
    • 2016-06-21
    • 2018-11-22
    • 1970-01-01
    • 2017-07-20
    • 2014-04-18
    • 1970-01-01
    • 1970-01-01
    • 2011-05-06
    相关资源
    最近更新 更多