为什么“如果不是(a和b)”比“如果不是a或不是b”快?

2024-10-03 02:45:30 发布

您现在位置:Python中文网/ 问答频道 /正文

我最近突发奇想,用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

…结果是:

^{pr2}$

效率的差异在1%到9%之间,总是有利于if not (a and b),这与我可能预期的相反,因为我知道if not a or not b将按顺序计算其项(if not a,然后if not b),一旦遇到真正的表达式(并且没有and子句),它将运行if块。相反,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(..)函数,以获得发布的结果。在


Tags: orandfromimportiftimenotcyc
2条回答

TL;DR

not_or_chk函数需要两个一元运算来对两个跳转进行加法运算(在最坏的情况下),而and_chk函数只有两个跳转(同样,在最坏的情况下)。在

细节

救命的dis moduledis模块允许您查看代码的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))

产生以下输出:

^{pr2}$

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

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

我刚刚通过你的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版本的时间差异。结果很有趣,伊莫。我也借此机会简化了你的测试程序。在

#!/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)

输出

^{pr2}$

这些计时是在运行mepis11(Debian系列Linux发行版)的2Ghzpentium4(单核32位)上使用python2.6.6执行的。在

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

此外,通常要重复几次定时循环,取最小值;FWIW,我通常重复3到5次。从timeit docs

Note

It’s tempting to calculate mean and standard deviation from the result vector and report these. However, this is not very useful. In a typical case, the lowest value gives a lower bound for how fast your machine can run the given code snippet; higher values in the result vector are typically not caused by variability in Python’s speed, but by other processes interfering with your timing accuracy. So the min() of the result is probably the only number you should be interested in. After that, you should look at the entire vector and apply common sense rather than statistics.

您的Meta post说您希望对其他微优化实验执行报告,因此您可能有兴趣看看我几个月前发布的一些代码,这些代码对factorial函数的各种实现进行时间测试。在

相关问题 更多 >