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


129

从此页面,我们知道:

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

输出为:

## 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®Xeon®CPU E5640 @ 2.67GHz上使用Python 2.7.6进行了测试。


8
更多反汇编的命令并不意味着更高的复杂性更低的代码。但是,看到您的timeit测试,我对此很感兴趣。
Marco Bonelli

6
我认为,“评估一次”带来的速度差异y不仅是变量查找,还包括函数调用等更昂贵的过程?即10 < max(range(100)) < 15快于10 < max(range(100)) and max(range(100)) < 15因为max(range(100))两次比较都被调用一次。
zehnpaard

2
@MarcoBonelli 当反汇编的代码1)不包含循环,并且2)每个字节码都非常快时,它执行此操作,因为这时mainloop的开销变得非常大。
2015年

2
分支预测可能会弄乱您的测试。
Corey Ogburn

2
@zehnpaard我同意你的看法。当“ y”大于一个简单值(例如,函数调用或计算)时,我希望对“ y”进行一次x <y <z求值的事实会产生更明显的影响。在出现的情况下,我们处于误差线之内:(失败的)分支预测的效果,小于最佳字节码的效果以及其他特定于平台/ CPU的效果占主导地位。这并没有使评估一次更好(以及更具可读性)的一般规则无效,但是表明这可能并没有增加极端的价值。
MartyMacGyver,2015年

Answers:


111

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

18
当然,在语义上也可能存在差异。y()不仅可以返回两个不同的值,而且使用变量对x <y中小于运算符的求值可以更改y的值。这就是为什么通常不会在字节码中对其进行优化(例如,如果y是一个非局部变量或一个参与闭包的变量)的原因
Random832

只是好奇,为什么需要sleep()在函数内部?
教授

@Prof这是模拟一个函数,该函数需要一些时间来计算结果。如果函数立即返回,则两个timeit结果之间不会有太大差异。
罗布

@Rob为什么没有太大的区别?这将是3毫秒和205毫秒,这足以说明这一点,不是吗?
教授

@Prof您缺少y()被两次计算的要点,因此它是2x200ms,而不是1x200ms。其余时间(3/5 ms)与定时测量无关。
罗布2015年

22

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

          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-与您可能应用的任何表达式级调整相比,在相同的工作量下将提供更多的改进。


首先,必须要在所有优化中最重要的是:内联。这与动态语言的“已解决问题”相去甚远,动态语言允许动态地更改函数的实现(但是可以这样做-HotSpot可以做类似的事情,而Graal之类的工作正在致力于使此类优化可用于Python和其他动态语言。 )。而且由于函数本身可能是从不同的模块调用的(或者调用可能是动态生成的!),所以您实际上不能在那里进行这些优化。
Voo 2015年

1
@Voo我的经过手动优化的字节码即使在存在任意动态性的情况下也具有与原始语义完全相同的语义(一个例外:假定a <b≡b> a)。此外,内联被高估了。如果您做太多事情-做太多事情太容易了-您就会炸毁I缓存并丢失所获得的一切,然后再失去一些。
zwol 2015年

没错,我认为您的意思是您想将您interesting_compare的代码优化为顶部的简单字节码(仅适用于内联)。完全偏离主题,但:对于任何编译器,内联都是最重要的优化之一。您可以尝试在实际程序上运行带有HotSpot的基准测试(不是某些数学测试将其99%的时间花费在经过手动优化的一个热循环中,因此无论如何也不再有任何函数调用),当您禁用它时内联任何东西的能力-您将看到大量的回归。
Voo 2015年

@Voo是的,顶部的简单字节码应该是OP原始代码的“最佳版本”,而不是interesting_compare
zwol

“一个例外:假定a <b≡b> a”→在Python中根本不成立。另外,我认为CPython甚至不能真正假设操作y不会改变堆栈,因为它具有许多调试工具。
Veedrac

8

由于输出的差异似乎是由于缺乏优化所致,所以我认为在大多数情况下您应该忽略该差异-可能差异会消失。区别在于,y只应评估一次,然后通过将其复制到堆栈上来解决该问题,这需要额外的费用POP_TOP- LOAD_FAST尽管有可能使用解决方案。

但是,重要的区别在于,如果对x<y and y<z第二个y进行评估,则如果应x<y为true,则应评估两次,如果对的评估y花费大量时间或具有副作用,则可能会产生影响。

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


6

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

x < y < z结构:

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

因此,请根据您想要的语义以及是否相等来选择一个,以代替另一个。

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

但要注意,这个结果并没有在更一般的情况下举行,仅在“最坏情况”那你碰巧轮廓。正如其他人指出的那样,如果更改y为花费更多时间的内容,您将看到结果更改,因为链接表示法仅对它进行一次评估。

总结:

  • 性能之前要考虑语义。
  • 考虑到可读性。
  • 不要相信微型基准。始终使用不同种类的参数进行分析,以了解功能/表达式时序相对于所述参数的行为,并考虑您打算如何使用它。

5
我认为您的答案不包含直接和相关的事实,即在问题的特殊情况下(引用浮点数),引用页面完全是错误的。从测量结果和生成的字节码中看,链式比较的速度并不快。
pvg 2015年

30
用“也许您不应该过多考虑性能”来回答一个标记性能的问题,对我来说似乎没有用。您正在对发问者对一般编程原理的掌握做出可能令人鼓舞的假设,然后主要谈论它们,而不是眼前的问题。
本·米尔伍德

@Veerdac,您在读错评论。OP所依赖的原始文档中提出的优化建议是错误的,这至少是浮点数的情况。它并不快。
pvg 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.