"x <y <z"比"x <y和y <z"快吗?

zan*_*ngw 128 python performance

这个页面,我们知道:

链式比较比使用and运算符更快.写x < y < z而不是x < y and y < z.

但是,我得到了不同的结果测试以下代码片段:

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

似乎x < y and y < z比它更快x < y < z.为什么?

在搜索了这个网站上的一些帖子之后(就像这个),我知道"仅评估一次"是关键x < y < 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)
Run Code Online (Sandbox Code Playgroud)

输出是:

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

似乎x < y and y < z没有更少的拆解命令x < y < z.我应该考虑x < y and y < z更快x < y < z吗?

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

Rob*_*Rob 110

不同之处在于,x < y < 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
Run Code Online (Sandbox Code Playgroud)

  • 当然,也可能存在语义差异.y()不仅可以返回两个不同的值,而且使用变量,x <y中的less-than运算符的求值可以改变y的值.这就是为什么它通常没有在字节代码中进行优化(例如,如果y是非局部变量或参与闭包的变量) (17认同)

zwo*_*wol 22

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

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

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

def interesting_compare(y):
    x = 1.1
    z = 1.3
    return x < y < z  # or: x < y and y < z
Run Code Online (Sandbox Code Playgroud)

同样,两个版本的比较在语义上是相同的,因此两个构造的最佳字节码是相同的.最好我可以解决它,它看起来像这样.我用每个操作码之前和之后的堆栈内容注释了每一行,用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  --
Run Code Online (Sandbox Code Playgroud)

如果语言的实现,CPython,PyPy,无论如何,都不会为这两种变体生成这个字节码(或它自己的等效操作序列),这表明该字节码编译器的质量很差.从你发布到上面的字节码序列获取是一个解决的问题(我认为你需要的是这种情况下的常量折叠,死代码消除,以及更好的堆栈内容建模; 常见的子表达式消除也很便宜且有价值),并没有理由不在现代语言实现中这样做.

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


sky*_*ing 8

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

然而,重要的区别在于,如果评估为真,x<y and y<z则第二个y应评估两次,如果x<y评估y需要相当长的时间或产生副作用,则会产生影响.

在大多数情况下你应该使用x<y<z尽管它有点慢.


Bak*_*riu 6

首先,你的比较几乎毫无意义,因为没有引入两种不同的结构来提供性能改进,所以你不应该根据它来决定是否使用一种结构代替另一种.

x < y < z结构:

  1. 它的含义更清晰,更直接.
  2. 它的语义是你对比较的"数学意义"所期望的:evalute x,y并且z 一次检查整个条件是否成立.通过多次and评估来改变语义y,这可以改变结果.

因此,根据您想要的语义选择一个代替另一个,如果它们是等价的,那么一个是否比另一个更可读.

这表示:更多的反汇编代码也并不意味着慢的代码.但是,执行更多的字节码操作意味着每个操作都更简单,但它需要主循环的迭代.这意味着如果您正在执行的操作非常快(例如,当您在那里执行局部变量查找)时,执行更多字节码操作的开销可能很重要.

但是请注意,这个结果并没有在更一般的情况下举行,只到你碰巧配置文件中的"最坏情况".正如其他人所指出的那样,如果你改变y到需要花费更多时间的东西,你会发现结果会发生变化,因为链式符号只会评估一次.

总结:

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

  • 回答标记性能的问题"也许你不应该考虑这么多性能"对我来说似乎没什么用.你对提问者对一般编程原则的掌握做出了潜在的光顾假设,然后主要讨论它们而不是手头的问题. (30认同)
  • 我认为你的答案不包括直截了当和相关的事实,即引用页面,在问题的特定情况下 - 比较浮点数,是完全错误的.在测量和生成的字节码中看到的链式比较并不快. (5认同)