关于python:“x < y < z” 比 “x < y and y < z” 更快吗

Is “x < y < z” faster than “x < y and y < z”?

从这一页,我们知道:

Chained comparisons are faster than using the and operator.
Write x < y < z instead of x < y and y < z.

但是,我对以下代码段进行了不同的测试:

1
2
3
4
5
6
7
8
$ 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分解了这两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

输出为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
## 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 [email protected]上使用python 2.7.6进行测试。


区别在于,在x < y < zy中,只评估一次。如果y是一个变量,这不会有很大的区别,但是当它是一个函数调用时,这需要一些时间来计算。

1
2
3
4
5
6
7
8
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


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

1
2
          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

因为没有使用比较结果。让我们返回比较结果,使情况更有趣。让我们也让结果在编译时无法知道。

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

同样,比较的两个版本在语义上是相同的,因此对于这两个构造,最佳字节码是相同的。尽我所能,它看起来像这样。我在每一个操作码前后用第四个符号(右边的栈顶,--在前后进行除法,后面的?表示可能存在或可能不存在的内容)为每一行添加了注释。注意,RETURN_VALUE丢弃了返回值下面堆栈上的所有内容。

1
2
3
4
5
6
7
8
          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(无论什么)没有为这两种变体生成这个字节码(或它自己的等价操作序列),这说明字节码编译器的质量很差。从你发布到上面的字节码序列中获取是一个解决了的问题(我认为这种情况下你所需要的就是不断的折叠、死代码消除和栈内容的更好的建模;常见的子表达式消除也将是廉价和有价值的),而且在现代语言impl中没有理由不这样做。喷发。

现在,所有当前的语言实现都有质量较差的字节码编译器。但是在编码时应该忽略这一点!假设字节码编译器是好的,并编写最可读的代码。无论如何,它可能足够快。如果不是的话,首先寻找算法上的改进,然后给cython第二次尝试——这将为相同的工作提供比你可能应用的任何表达式级别的调整更多的改进。


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

不过,重要的区别在于,在x中,如果x的评估结果为真,则第二个y应进行两次评估,如果y的评估需要相当长的时间或有副作用,则这具有一定的意义。

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


首先,您的比较几乎没有意义,因为没有引入两个不同的结构来提高性能,所以您不应该基于这一点来决定是否使用一个结构代替另一个结构。

x < y < z构造:

  • 它的意思更清楚、更直接。
  • 它的语义是您从比较的"数学意义"中所期望的:对xyz进行一次评估,并检查整个条件是否成立。使用and通过多次评估y来改变语义,从而改变结果。
  • 因此,根据您想要的语义,选择一个代替另一个,如果它们是等效的,那么选择一个是否比另一个更可读。

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

    但是请注意,这个结果不适用于更一般的情况,只适用于您碰巧分析的"最坏情况"。正如其他人所指出的,如果您将y更改为需要更多时间的内容,您将看到结果会更改,因为链式表示法只对其进行一次评估。

    总结:

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