【问题标题】:Is "x < y < z" faster than "x < y and y < z"?“x < y < z”比“x < y and y < z”快吗?
【发布时间】:2015-12-01 07:31:35
【问题描述】:

this page,我们知道:

链式比较比使用and 运算符更快。 写x &lt; y &lt; z 而不是x &lt; y and y &lt; z

但是,我在测试以下代码 sn-ps 时得到了不同的结果:

$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y < z"
1000000 loops, best of 3: 0.322 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y and y < z"
1000000 loops, best of 3: 0.22 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y < z"
1000000 loops, best of 3: 0.279 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y and y < z"
1000000 loops, best of 3: 0.215 usec per loop

似乎x &lt; y and y &lt; zx &lt; y &lt; z 快。 为什么?

在这个网站上搜索了一些帖子(如this one)后,我知道“只评估一次”是x &lt; y &lt; z 的关键,但我仍然感到困惑。为了进一步研究,我使用dis.dis对这两个函数进行了反汇编:

import dis

def chained_compare():
        x = 1.2
        y = 1.3
        z = 1.1
        x < y < z

def and_compare():
        x = 1.2
        y = 1.3
        z = 1.1
        x < y and y < z

dis.dis(chained_compare)
dis.dis(and_compare)

输出是:

## chained_compare ##

  4           0 LOAD_CONST               1 (1.2)
              3 STORE_FAST               0 (x)

  5           6 LOAD_CONST               2 (1.3)
              9 STORE_FAST               1 (y)

  6          12 LOAD_CONST               3 (1.1)
             15 STORE_FAST               2 (z)

  7          18 LOAD_FAST                0 (x)
             21 LOAD_FAST                1 (y)
             24 DUP_TOP
             25 ROT_THREE
             26 COMPARE_OP               0 (<)
             29 JUMP_IF_FALSE_OR_POP    41
             32 LOAD_FAST                2 (z)
             35 COMPARE_OP               0 (<)
             38 JUMP_FORWARD             2 (to 43)
        >>   41 ROT_TWO
             42 POP_TOP
        >>   43 POP_TOP
             44 LOAD_CONST               0 (None)
             47 RETURN_VALUE

## and_compare ##

 10           0 LOAD_CONST               1 (1.2)
              3 STORE_FAST               0 (x)

 11           6 LOAD_CONST               2 (1.3)
              9 STORE_FAST               1 (y)

 12          12 LOAD_CONST               3 (1.1)
             15 STORE_FAST               2 (z)

 13          18 LOAD_FAST                0 (x)
             21 LOAD_FAST                1 (y)
             24 COMPARE_OP               0 (<)
             27 JUMP_IF_FALSE_OR_POP    39
             30 LOAD_FAST                1 (y)
             33 LOAD_FAST                2 (z)
             36 COMPARE_OP               0 (<)
        >>   39 POP_TOP
             40 LOAD_CONST               0 (None)

似乎x &lt; y and y &lt; z 的反汇编命令比x &lt; y &lt; z 少。我应该考虑x &lt; y and y &lt; zx &lt; y &lt; z 快吗?

在 Intel(R) Xeon(R) CPU E5640 @ 2.67GHz 上使用 Python 2.7.6 进行测试。

【问题讨论】:

  • 更多反汇编的命令并不意味着更复杂也不意味着更慢的代码。但是看到你的timeit 测试我对此很感兴趣。
  • y 不仅仅是一个变量查找,而是一个更昂贵的过程(如函数调用)时,我假设与“评估一次”的速度差异出现了? IE。 10 &lt; max(range(100)) &lt; 1510 &lt; max(range(100)) and max(range(100)) &lt; 15 快,因为两次比较都调用了一次 max(range(100))
  • @MarcoBonelli 它确实当反汇编代码 1) 不包含循环和 2) 每个字节码都非常快,因为此时主循环的开销变得很重要。
  • 分支预测可能会弄乱您的测试。
  • @zehnpaard 我同意你的看法。当“y”不仅仅是一个简单的值(例如,函数调用或计算)时,我希望“y”在 x

标签: python performance


【解决方案1】:

不同之处在于x &lt; y &lt; z y 只计算一次。如果 y 是一个变量,这并没有太大的区别,但是当它是一个函数调用时,它会产生很大的差异,这需要一些时间来计算。

from time import sleep
def y():
    sleep(.2)
    return 1.3
%timeit 1.2 < y() < 1.8
10 loops, best of 3: 203 ms per loop
%timeit 1.2 < y() and y() < 1.8
1 loops, best of 3: 405 ms per loop

【讨论】:

  • 当然,也可能存在语义差异。 y() 不仅可以返回两个不同的值,而且对于一个变量,x
  • 只是好奇,为什么你需要一个sleep() 在函数内部?
  • @Prof 那是模拟一个需要一些时间来计算结果的函数。如果函数立即返回,则两个 timeit 结果之间不会有太大差异。
  • @Rob 为什么没有太大区别?这将是 3 毫秒 vs 205 毫秒,这足以证明它不是吗?
  • @Prof 你错过了 y() 计算两次的点,所以它是 2x200ms 而不是 1x200ms。其余(3/5 ms)是定时测量中的无关噪声。
【解决方案2】:

最佳您定义的两个函数的字节码将是

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

因为没有使用比较的结果。让我们通过返回比较结果使情况更有趣。让我们也让结果在编译时不可知。

def interesting_compare(y):
    x = 1.1
    z = 1.3
    return x < y < z  # or: x < y and y < z

再次,比较的两个版本在语义上是相同的,所以 最佳 字节码对于两个结构是相同的。尽我所能,它看起来像这样。我用 Forth 表示法在每个操作码之前和之后用堆栈内容注释了每一行(右侧堆栈顶部,-- 前后除,尾​​随 ? 表示可能存在或不存在的内容)。请注意,RETURN_VALUE 会丢弃碰巧留在堆栈中返回值下方的所有内容。

          0 LOAD_FAST                0 (y)    ;          -- y
          3 DUP_TOP                           ; y        -- y y
          4 LOAD_CONST               0 (1.1)  ; y y      -- y y 1.1
          7 COMPARE_OP               4 (>)    ; y y 1.1  -- y pred
         10 JUMP_IF_FALSE_OR_POP     19       ; y pred   -- y
         13 LOAD_CONST               1 (1.3)  ; y        -- y 1.3
         16 COMPARE_OP               0 (<)    ; y 1.3    -- pred
     >>  19 RETURN_VALUE                      ; y? pred  --

如果 CPython、PyPy 等语言的实现不为这两种变体生成此字节码(或它自己的等效操作序列),这表明该字节码编译器的质量很差 .从您发布到上面的字节码序列中获取是一个已解决的问题(我认为这种情况下您需要的只是constant foldingdead code elimination,以及更好的堆栈内容建模;common subexpression elimination 也很便​​宜而且有价值的),并且没有理由不在现代语言实现中这样做。

现在,该语言的所有当前实现都具有质量较差的字节码编译器。但是您应该在编码时忽略!假装字节码编译器不错,写出最可读的代码。无论如何,它可能会足够快。如果不是,请先寻找算法改进,然后再试一试Cython——这将比您可能应用的任何表达式级调整提供更多改进。

【讨论】:

  • 所有优化中最重要的首先必须成为可能:内联。对于允许动态更改函数的实现的动态语言来说,这远非“已解决的问题”(尽管可行——HotSpot 可以做类似的事情,而 Graal 之类的事情正在努力使这些优化可用于 Python 和其他动态语言)。而且由于函数本身可能会从不同的模块调用(或者调用可能会动态生成!)你真的不能在那里进行这些优化。
  • @Voo 即使存在任意动态,我的手动优化字节码也具有与原始字节码完全相同的语义(假设有一个例外:a a)。此外,内联被高估了。如果你做的太多——而且做太多太容易——你会破坏 I-cache 并失去你所获得的一切,然后是一些。
  • 你说得对,我认为你的意思是你想优化你的 interesting_compare 到顶部的简单字节码(这只适用于内联)。完全题外话,但是:内联是任何编译器最重要的优化之一。当您禁用它时,您可以尝试使用 HotSpot 在真实程序上运行一些基准测试(而不是一些数学测试,它们将 99% 的时间花在一个手动优化的热循环中 [因此无论如何都没有更多的函数调用])内联任何东西的能力 - 你会看到大量的回归。
  • @Voo 是的,顶部的简单字节码应该是 OP 原始代码的“最佳版本”,而不是 interesting_compare
  • “一个例外:假设 a a” → 这在 Python 中根本不是真的。另外,我认为 CPython 甚至不能真正假设 y 上的操作不会改变堆栈,因为它有很多调试工具。
【解决方案3】:

由于输出的差异似乎是由于缺乏优化,我认为在大多数情况下您应该忽略这种差异 - 差异可能会消失。不同之处在于,y 只应评估一次,并且通过在堆栈上复制它来解决,这需要额外的POP_TOP - 不过使用LOAD_FAST 的解决方案可能是可能的。

但重要的区别在于,在 x&lt;y and y&lt;z 中,如果 x&lt;y 的计算结果为 true,则第二个 y 应该被计算两次,如果 y 的计算需要相当长的时间或有副作用,这会产生影响。

在大多数情况下,您应该使用x&lt;y&lt;z,尽管它有点慢。

【讨论】:

    【解决方案4】:

    首先,您的比较毫无意义,因为没有引入两种不同的结构来提供性能改进,因此您不应该决定是否使用一个来代替另一个基于关于那个。

    x &lt; y &lt; z 构造:

    1. 含义更清晰、更直接。
    2. 它的语义是您对比较的“数学意义”所期望的:评估xyz 一次 并检查整个条件是否成立。使用 and 通过多次评估 y 来改变语义,这可以改变结果

    因此,根据您想要的语义选择一个代替另一个,如果它们是等效的,是否一个比另一个更具可读性。

    这就是说:更多的反汇编代码确实意味着更慢的代码。 然而,执行更多的字节码操作意味着每个操作更简单,但它需要主循环的迭代。 这意味着如果您正在执行的操作非常快(例如,您正在那里进行局部变量查找),那么执行更多字节码操作的开销可能很重要。

    但请注意,此结果适用于更一般的情况,仅适用于您碰巧描述的“最坏情况”。 正如其他人所指出的那样,如果您将 y 更改为需要更多时间的东西,您会看到结果发生了变化,因为链式表示法只计算一次。

    总结:

    • 在性能之前考虑语义。
    • 考虑可读性。
    • 不要相信微基准。始终使用不同类型的参数进行分析,以查看函数/表达式时序与所述参数相关的行为,并考虑您打算如何使用它。

    【讨论】:

    • 我认为您的回答不包括直接和相关的事实,即在问题的特定情况下引用的页面 - 比较浮点数是完全错误的。从测量和生成的字节码中都可以看出,链式比较并不快。
    • 用“也许你不应该过多地考虑性能”来回答带有性能标签的问题对我来说似乎没有用。你对提问者对一般编程原则的掌握做出了潜在的傲慢假设,然后主要谈论它们而不是手头的问题。
    • @Veerdac 你误读了评论。 OP 所依赖的原始文档中建议的优化是错误的,至少在浮动的情况下。它并不快。
    猜你喜欢
    • 1970-01-01
    • 2012-10-07
    • 2013-02-26
    • 1970-01-01
    • 2022-01-12
    • 2012-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多