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

这一页我们知道:

链式比较比使用 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

看起来 x < y and y < zx < 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)

结果是:

## 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 < z的掩饰命令比 x < y < z少。我应该考虑 x < y and y < zx < y < z快吗?

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

10853 次浏览

区别在于,在 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

首先,您的比较几乎没有意义,因为引入了两种不同的结构来提供性能改进,所以您不应该根据这一点来决定是否使用其中一种结构来代替另一种结构。

x < y < z结构:

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

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

这就是说: 反汇编代码越多,没有就意味着代码越慢。 然而,执行更多的字节码操作意味着每个操作更简单,但是它需要对主循环进行迭代。 这意味着您正在执行的 如果操作非常快(例如,您正在执行的本地变量查找) ,那么执行更多字节码操作的 在头顶上就很重要。

但是请注意,这个结果是 没有在更一般的情况下,只有“最坏的情况下”,你碰巧配置文件举行。 正如其他人所指出的,如果您将 y更改为需要花费更多时间的内容,那么您将看到结果发生了变化,因为链表示法只计算它一次。

总结:

  • 在执行之前要考虑语义。
  • 考虑可读性。
  • 不要相信微观基准。始终使用不同类型的参数进行配置,以了解函数/表达式计时与所述参数的关系,并考虑如何计划使用它。

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

但是重要的区别是,在 x<y and y<z中,如果 x<y评估为真,第二个 y应该评估两次,如果评估 y需要相当长的时间或有副作用,这就意味着。

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

对于您定义的两个函数,最佳的 字节码应该是

          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 等等没有为这两种变体生成这个字节码(或它自己的等效操作序列) ,那么就是 它演示了字节码编译器的低质量。从你发布到上面的字节码序列中获取是一个已经解决的问题(我认为所有你需要的是 常数折叠死码删除,和更好的栈内容建模; 公共子表达式消除也将是廉价和有价值的) ,并且真的没有理由不在一个现代语言实现中这样做。

现在,这种语言的所有当前实现都有质量差的字节码编译器。但是你应该 忽略,而编码!假装字节码编译器是好的,并编写最 可读代码。不管怎样,它可能已经足够快了。如果不是,那么首先寻找算法改进,然后再给 Cython一次尝试的机会——这将为同样的工作提供比您可能应用的任何表达式级调整更多的改进。