【问题标题】:Why is "if not (a and b)" faster than "if not a or not b"?为什么“if not (a and b)”比“if not a or not b”快?
【发布时间】:2015-06-15 13:59:02
【问题描述】:

一时兴起,我最近用timeit测试了这两种方法,看看哪种评估方法更快:

import timeit

"""Test method returns True if either argument is falsey, else False."""

def and_chk((a, b)):
    if not (a and b):
        return True
    return False

def not_or_chk((a, b)):
    if not a or not b:
        return True
    return False

...得到了这些结果:

 VALUES FOR a,b ->      0,0         0,1         1,0         1,1
        method
    and_chk(a,b)    0.95559     0.98646     0.95138     0.98788
 not_or_chk(a,b)    0.96804     1.07323     0.96015     1.05874
                                            ...seconds per 1,111,111 cycles.

效率差异在百分之一到百分之九之间,始终支持if not (a and b),这与我的预期相反,因为我知道if not a or not b 将评估其条款(if not a 然后@987654327 @) 按顺序运行if 块,一旦遇到真表达式(并且没有and 子句)。相比之下,and_chk 方法需要评估 两个 子句,然后才能将任何结果返回给包装它的 if not..

然而,计时结果反驳了这种理解。那么,如何评估 if 条件?我完全清楚这样一个事实,即这种程度的微优化实际上(即使不是完全)毫无意义。我只是想了解 Python 是怎么做的。


为了完整起见,这就是我设置timeit...的方式...

cyc = 1111111

bothFalse_and = iter([(0,0)] * cyc)
zeroTrue_and = iter([(1,0)] * cyc)
oneTrue_and = iter([(0,1)] * cyc)
bothTrue_and = iter([(1,1)] * cyc)

bothFalse_notor = iter([(0,0)] * cyc)
zeroTrue_notor = iter([(1,0)] * cyc)
oneTrue_notor = iter([(0,1)] * cyc)
bothTrue_notor = iter([(1,1)] * cyc)

time_bothFalse_and = timeit.Timer('and_chk(next(tups))', 'from __main__ import bothFalse_and as tups, and_chk')
time_zeroTrue_and = timeit.Timer('and_chk(next(tups))', 'from __main__ import zeroTrue_and as tups, and_chk')
time_oneTrue_and = timeit.Timer('and_chk(next(tups))', 'from __main__ import oneTrue_and as tups, and_chk')
time_bothTrue_and = timeit.Timer('and_chk(next(tups))', 'from __main__ import bothTrue_and as tups, and_chk')

time_bothFalse_notor = timeit.Timer('not_or_chk(next(tups))', 'from __main__ import bothFalse_notor as tups, not_or_chk')
time_zeroTrue_notor = timeit.Timer('not_or_chk(next(tups))', 'from __main__ import zeroTrue_notor as tups, not_or_chk')
time_oneTrue_notor = timeit.Timer('not_or_chk(next(tups))', 'from __main__ import oneTrue_notor as tups, not_or_chk')
time_bothTrue_notor = timeit.Timer('not_or_chk(next(tups))', 'from __main__ import bothTrue_notor as tups, not_or_chk')

...然后使用.timeit(cyc) 运行每个timeit.Timer(..) 函数以发布结果。

【问题讨论】:

  • 不是很明显吗?运算符较少。
  • 它们应该以同样的方式短路:如果aFalse 而不评估band 应该停止,如果not a 为真,or 应该停止,不评估not b
  • @Mephy,OP 没有征求编程建议。
  • Python 是一种语言规范,并且有一些实现可以解释您的源文件并运行它。既然您提到了 IDLE,我假设您使用的是 CPython,这是一个用 C 编写的 Python 解释器。如果他们想深入了解内部结构以理解您的结果,这可能会有所帮助。
  • dis 模块,它显示 Python 字节码(一种高级汇编语言,如果您不熟悉的话)可能会有所帮助。试试import dis; print(dis.dis(f))f 是你的函数。

标签: python python-2.7 if-statement logical-operators micro-optimization


【解决方案1】:

TL;DR

not_or_chk 函数需要两个一元操作,除了两次跳转(在最坏的情况下),而and_chk 函数只有两次跳转(同样,在最坏的情况下) .

详情

dis module 来救援了! dis 模块可让您查看代码的 Python 字节码反汇编。例如:

import dis

"""Test method returns True if either argument is falsey, else False."""

def and_chk((a, b)):
    if not (a and b):
        return True
    return False

def not_or_chk((a, b)):
    if not a or not b:
        return True
    return False

print("And Check:\n")
print(dis.dis(and_chk))

print("Or Check:\n")
print(dis.dis(not_or_chk))

产生这个输出:

And Check:

  5           0 LOAD_FAST                0 (.0)
              3 UNPACK_SEQUENCE          2
              6 STORE_FAST               1 (a)
              9 STORE_FAST               2 (b)

  6          12 LOAD_FAST                1 (a)    * This block is the *
             15 JUMP_IF_FALSE_OR_POP    21        * disassembly of    *
             18 LOAD_FAST                2 (b)    * the "and_chk"     *
        >>   21 POP_JUMP_IF_TRUE        28        * function          *

  7          24 LOAD_GLOBAL              0 (True)
             27 RETURN_VALUE

  8     >>   28 LOAD_GLOBAL              1 (False)
             31 RETURN_VALUE
None
Or Check:

 10           0 LOAD_FAST                0 (.0)
              3 UNPACK_SEQUENCE          2
              6 STORE_FAST               1 (a)
              9 STORE_FAST               2 (b)

 11          12 LOAD_FAST                1 (a)    * This block is the *
             15 UNARY_NOT                         * disassembly of    *
             16 POP_JUMP_IF_TRUE        26        * the "not_or_chk"  *
             19 LOAD_FAST                2 (b)    * function          *
             22 UNARY_NOT
             23 POP_JUMP_IF_FALSE       30

 12     >>   26 LOAD_GLOBAL              0 (True)
             29 RETURN_VALUE

 13     >>   30 LOAD_GLOBAL              1 (False)
             33 RETURN_VALUE
None

看看我用星号标记的两个 Python 字节码块。这些块是你的两个反汇编函数。注意and_chk只有两次跳转,函数中的计算是在决定是否跳转的时候进行的

另一方面,not_or_chk函数需要在最坏的情况下执行两次not操作,另外解释器决定是否进行跳转。

【讨论】:

  • 这真是令人着迷!谢谢你给我看这样的东西。我没有想到 not 是一个操作员(之前已经指出,我意识到这是一个愚蠢的事情),它完全解释了正在发生的事情。不仅如此,我想我会和dis 一起玩得很开心,所以也谢谢你! :D
【解决方案2】:

我刚刚通过您的 Meta SO 问题注意到了这个问题:Is it appropriate to share the results of my research toward solving my own minor questions?。正如您在那个问题中提到的(并且这个问题上的一个标签表明),这种事情属于微优化的范畴。理想情况下,人们不必担心如此细微的性能差异,正如 Knuth 所说,premature optimization is the root of all evil。但我想调查这些问题是有趣且有启发性的,因为它可以让您更好地了解 Python 是如何“在幕后”工作的。

Mephy's comment 提示我查看if-less 函数版本的时间差异。结果很有趣,恕我直言。我还借此机会简化了您的测试程序。

#!/usr/bin/env python

''' Do timeit tests on various implementations of NAND

    NAND returns True if either argument is falsey, else False.
    From https://stackoverflow.com/q/29551438/4014959
    Written by PM 2Ring 2015.04.09
'''

from timeit import Timer
import dis

def and_chk(a, b):
    return not (a and b)

def and_chk_if(a, b):
    if not (a and b):
        return True
    else:
        return False

def not_or_chk(a, b):
    return not a or not b

def not_or_chk_if(a, b):
    if not a or not b:
        return True
    else:
        return False


#All the functions
funcs = (
    and_chk,
    and_chk_if,
    not_or_chk,
    not_or_chk_if,
)

#Argument tuples to test the functions with
bools = (0, 1)
bool_tups = [(u, v) for u in bools for v in bools]


def show_dis():
    ''' Show the disassembly for each function '''
    print 'Disassembly'
    for func in funcs:
        fname = func.func_name
        print '\n%s' % fname
        dis.dis(func)
    print


def verify():
    ''' Verify that the functions actually perform as intended '''
    print 'Verifying...'
    for func in funcs:
        fname = func.func_name
        print '\n%s' % fname
        for args in bool_tups:
            print args, func(*args)
    print


def time_test(loops, reps):
    ''' Print timing stats for all the functions '''
    print 'Timing tests\nLoops = %d, Repetitions = %d' % (loops, reps)

    for func in funcs:
        fname = func.func_name
        print '\n%s' % fname
        setup = 'from __main__ import %s' % fname
        for args in bool_tups:
            t = Timer('%s%s' % (fname, args), setup)
            r = t.repeat(reps, loops)
            r.sort()
            print args, r


show_dis()
verify()
time_test(loops=520000, reps=3)

输出

Disassembly

and_chk
 13           0 LOAD_FAST                0 (a)
              3 JUMP_IF_FALSE            4 (to 10)
              6 POP_TOP             
              7 LOAD_FAST                1 (b)
        >>   10 UNARY_NOT           
             11 RETURN_VALUE        

and_chk_if
 16           0 LOAD_FAST                0 (a)
              3 JUMP_IF_FALSE            4 (to 10)
              6 POP_TOP             
              7 LOAD_FAST                1 (b)
        >>   10 JUMP_IF_TRUE             5 (to 18)
             13 POP_TOP             

 17          14 LOAD_GLOBAL              0 (True)
             17 RETURN_VALUE        
        >>   18 POP_TOP             

 19          19 LOAD_GLOBAL              1 (False)
             22 RETURN_VALUE        
             23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        

not_or_chk
 22           0 LOAD_FAST                0 (a)
              3 UNARY_NOT           
              4 JUMP_IF_TRUE             5 (to 12)
              7 POP_TOP             
              8 LOAD_FAST                1 (b)
             11 UNARY_NOT           
        >>   12 RETURN_VALUE        

not_or_chk_if
 25           0 LOAD_FAST                0 (a)
              3 UNARY_NOT           
              4 JUMP_IF_TRUE             8 (to 15)
              7 POP_TOP             
              8 LOAD_FAST                1 (b)
             11 UNARY_NOT           
             12 JUMP_IF_FALSE            5 (to 20)
        >>   15 POP_TOP             

 26          16 LOAD_GLOBAL              0 (True)
             19 RETURN_VALUE        
        >>   20 POP_TOP             

 28          21 LOAD_GLOBAL              1 (False)
             24 RETURN_VALUE        
             25 LOAD_CONST               0 (None)
             28 RETURN_VALUE        

Verifying...

and_chk
(0, 0) True
(0, 1) True
(1, 0) True
(1, 1) False

and_chk_if
(0, 0) True
(0, 1) True
(1, 0) True
(1, 1) False

not_or_chk
(0, 0) True
(0, 1) True
(1, 0) True
(1, 1) False

not_or_chk_if
(0, 0) True
(0, 1) True
(1, 0) True
(1, 1) False

Timing tests
Loops = 520000, Repetitions = 3

and_chk
(0, 0) [0.36773014068603516, 0.37793493270874023, 0.38387489318847656]
(0, 1) [0.36292791366577148, 0.37119913101196289, 0.37146902084350586]
(1, 0) [0.38673520088195801, 0.39340090751647949, 0.39670205116271973]
(1, 1) [0.38938498497009277, 0.39505791664123535, 0.40034103393554688]

and_chk_if
(0, 0) [0.4021449089050293, 0.40345501899719238, 0.41558098793029785]
(0, 1) [0.40686416625976562, 0.41213202476501465, 0.44800615310668945]
(1, 0) [0.4281308650970459, 0.42916202545166016, 0.43681907653808594]
(1, 1) [0.46246123313903809, 0.46759700775146484, 0.47544980049133301]

not_or_chk
(0, 0) [0.35435700416564941, 0.36368083953857422, 0.36867713928222656]
(0, 1) [0.35602092742919922, 0.35732793807983398, 0.36237406730651855]
(1, 0) [0.39550518989562988, 0.40660715103149414, 0.40977287292480469]
(1, 1) [0.4060060977935791, 0.4112389087677002, 0.41296815872192383]

not_or_chk_if
(0, 0) [0.4308779239654541, 0.44109201431274414, 0.45827698707580566]
(0, 1) [0.43600606918334961, 0.4370419979095459, 0.47623395919799805]
(1, 0) [0.48452401161193848, 0.48769593238830566, 0.49147915840148926]
(1, 1) [0.53107500076293945, 0.54048299789428711, 0.55434417724609375]

这些计时是在运行 Mepis 11(Debian 系列 Linux 发行版)的 2GHz Pentium 4(单核 32 位)上使用 Python 2.6.6 执行的。

您会注意到,我避免使用您的 next(tups) 策略来获取每个函数调用的参数,而是直接将参数作为常量传递。执行next(tups) 所花费的时间应该相当稳定,但最好在实际情况下消除此类开销,以便报告的时间测量更准确地反映我们正在执行的代码的性能真的感兴趣。

此外,通常会执行多次计时循环并取最小值; FWIW,我一般做 3 到 5 次。来自timeit docs

注意

根据结果计算均值和标准差是很诱人的 矢量并报告这些。但是,这不是很有用。在一个 典型情况下,最低值给出了你的速度有多快的下限 机器可以运行给定的代码sn -p;结果中的更高值 矢量通常不是由 Python 速度的可变性引起的,而是 其他进程干扰您的计时精度。所以 min() 的结果可能是您应该感兴趣的唯一数字。 之后,您应该查看整个向量并应用 common 感觉而不是统计。

您的 Meta 帖子说您想要执行和报告其他微优化实验,因此您可能有兴趣查看我几个月前发布的一些代码,这些代码对 @987654325 的各种实现进行时间测试@函数。

【讨论】:

  • 我还注意到您在原始代码中添加了else 块。最初,我基于“如果你还在问,那是假的”/“如果它是其他任何东西,你现在已经退回它”而忽略了这个,但有点怀疑,因为它看起来像一些好的(或至少“更好的”)实践将包括在内。 ("Don't bother!" says this other post. ;) ) 无论如何,根据 Meta 帖子,感谢您的意见!我会仔细查看您在此处发布的内容(特别是回复:next(tups)!),看看它如何影响早期的一些测试。
  • @Augusta:不过,其他帖子的情况略有不同。它在争论else: pass 的优点,而这里我们有一个else,由于关联的if 块包含return,所以这是多余的。
  • @Augusta:如果您查看_chk_if 函数的dis 输出,您会发现它们有3 RETURN_VALUE 命令。除了 TrueFalse 返回之外,还添加了 return None,因为这是任何没有明确无条件返回值的 Python 函数的默认返回,并且解释器不够聪明,无法计算该函数必须通过if...else 的一个或另一个分支返回。 :)
  • 我认为另一篇文章略有不同,但我还假设如果else: pass 是可以忘记的,那么else: DoTheInevitableThing() 就像做不可避免的事情一样好。 ;) 实际上,我没有注意到那里有额外的 RETURN_VALUE 命令!但这是一种改进还是一种责任?我假设,因为 RETURN_VALUE 步骤是严格无法访问的,它们实际上并没有给代码添加任何“权重”,但即便如此,它有用吗?我并不是说它没有任何优点,我只是(还不)知道它是什么。 :)
  • @Augusta:是的。 else: DoTheInevitableThing() 很傻。 :) 而那些额外的LOAD_CONST 0 (None); RETURN_VALUE 步骤会使字节码略微膨胀,但它们实际上对性能没有影响,因为正如你所说,它们是不可访问的。尽管如此,最好构造你的函数,以便总是有一个无条件的返回,除非你想要自动附加return None。顺便说一句,您应该查看SO Python Chat Room
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-14
  • 1970-01-01
  • 1970-01-01
  • 2018-01-29
  • 2021-11-28
  • 1970-01-01
相关资源
最近更新 更多