“x <y <z”比“x <y和y <z”更快吗?

从这个页面 ,我们知道:

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

不过,我得到了一个不同的结果testing下面的代码片段:

 $ 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 < y and y < zx < y < z快。 为什么?

在这个网站上search一些post后(像这样 ),我知道“只评估一次”是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) 

输出是:

 ## 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 < y and y < zx < y < z有更less的分解命令。 我应该考虑x < y and y < z快于x < y < z吗?

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

不同之处在于x < y < z y只被评估一次。 如果y是一个variables,这并没有太大的区别,但是当它是一个函数调用时,这需要一些时间来计算。

 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 

你定义的两个函数都是最佳的字节码

  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 -- yy 4 LOAD_CONST 0 (1.1) ; yy -- yy 1.1 7 COMPARE_OP 4 (>) ; yy 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,不pipe这两个变体是不是生成这个字节码(或者它自己的等同的操作序列), 那么这个字节码编译器的质量就差了 。 从发布到上面的字节码序列中获得是一个解决的问题(我认为在这种情况下,您所需要的只是不断的折叠 , 死代码消除以及对堆栈内容的更好的build模; 常见的子expression式消除也是便宜且有价值的),在现代语言实现中没有这样做是没有借口的。

现在,这种语言的所有当前实现都具有质量差的字节码编译器。 但编码时你应该忽略这一点! 假装字节码编译器是好的,并写出最可读的代码。 无论如何,它可能足够快。 如果不是的话,首先寻找algorithm的改进,然后再给Cython一个尝试 – 相比于你可能使用的任何expression式级别的调整,这将为同样的努力提供更多的改进。

由于输出的差异似乎是由于缺乏优化,我认为你应该忽略大多数情况下的差异 – 这可能会导致差异消失。 区别是因为y只应该被评估一次,这是通过在需要额外的POP_TOP的堆栈上复制来解决的 – LOAD_FAST也可以使用LOAD_FAST的解决scheme。

然而,重要的区别在于,如果x<y and y<z ,那么如果x<y计算结果为true,则应该对第二个y进行两次评估,这对x<y计算是否需要相当长的时间或有副作用有影响。

在大多数情况下,尽pipe速度稍慢,但应该使用x<y<z

首先,你的比较是非常没有意义的,因为这两个不同的构造没有被引入来提供一个性能改进,所以你不应该决定是否使用一个来代替另一个。

x < y < z构造:

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

因此,根据你想要的语义select一个替代另一个, 如果它们相同,是否比另一个更可读。

这就是说:更多的反汇编代码并不意味着更慢的代码。 但是,执行更多的字节码操作意味着每个操作都比较简单,但是需要迭代主循环。 这意味着, 如果您正在执行的操作非常快(例如,您正在执行本地variables查找),那么执行更多字节码操作的开销可能很重要。

但是请注意,这个结果并不适用于更一般的情况,而只适用于你所描述的“最糟糕的情况”。 正如其他人所指出的那样,如果将y更改为需要更多时间的事情,则会看到结果会发生变化,因为链接符号只会计算一次。

总结:

  • 考虑性能之前的语义。
  • 考虑到可读性。
  • 不要相信微基准。 始终使用不同types的参数进行configuration,以查看函数/expression式计时与所述参数的关系,并考虑计划如何使用它。