为什么加法和乘法比比较更快?

时间:2017-01-24 15:31:12

标签: python performance

我一直认为比较是计算机可以执行的最快的操作。我记得在D. Knuth的演讲中听到它,他在那里按降序编写循环"因为与0的比较很快"。我还读到乘法应该比加法here慢。

我很惊讶地看到,在Python 2和Mac下,在Linux和Mac下进行测试,比较似乎比算术运算要慢得多。

有人可以解释原因吗?

%timeit 2 > 0
10000000 loops, best of 3: 41.5 ns per loop

%timeit 2 * 2
10000000 loops, best of 3: 27 ns per loop

%timeit 2 * 0
10000000 loops, best of 3: 27.7 ns per loop

%timeit True != False
10000000 loops, best of 3: 75 ns per loop

%timeit True and False
10000000 loops, best of 3: 58.8 ns per loop

在python 3下:

$ ipython3
Python 3.5.2 | packaged by conda-forge | (default, Sep  8 2016, 14:36:38) 
Type "copyright", "credits" or "license" for more information.

IPython 5.1.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: %timeit 2 + 2
10000000 loops, best of 3: 22.9 ns per loop

In [2]: %timeit 2 * 2
10000000 loops, best of 3: 23.7 ns per loop

In [3]: %timeit 2 > 2
10000000 loops, best of 3: 45.5 ns per loop

In [4]: %timeit True and False
10000000 loops, best of 3: 62.8 ns per loop

In [5]: %timeit True != False
10000000 loops, best of 3: 92.9 ns per loop

6 个答案:

答案 0 :(得分:19)

由于Python编译器中Constant Folding Peep Hole中的optimizer,会发生这种情况。

使用dis模块,如果我们打破每个语句以查看它们在机器级别的翻译方式,你会发现所有运算符如不等式,相等等首先被加载到内存中然后进行评估。但是,计算乘法,加法等所有表达式并将其作为常量加载到内存中。

总的来说,这导致执行步骤的数量减少,从而加快了步骤:

>>> import dis

>>> def m1(): True != False
>>> dis.dis(m1)
  1           0 LOAD_GLOBAL              0 (True)
              3 LOAD_GLOBAL              1 (False)
              6 COMPARE_OP               3 (!=)
              9 POP_TOP             
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

>>> def m2(): 2 *2
>>> dis.dis(m2)
  1           0 LOAD_CONST               2 (4)
              3 POP_TOP             
              4 LOAD_CONST               0 (None)
              7 RETURN_VALUE        

>>> def m3(): 2*5
>>> dis.dis(m3)
  1           0 LOAD_CONST               3 (10)
              3 POP_TOP             
              4 LOAD_CONST               0 (None)
              7 RETURN_VALUE        

>>> def m4(): 2 > 0
>>> dis.dis(m4)
  1           0 LOAD_CONST               1 (2)
              3 LOAD_CONST               2 (0)
              6 COMPARE_OP               4 (>)
              9 POP_TOP             
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

>>> def m5(): True and False
>>> dis.dis(m5)
  1           0 LOAD_GLOBAL              0 (True)
              3 JUMP_IF_FALSE_OR_POP     9
              6 LOAD_GLOBAL              1 (False)
        >>    9 POP_TOP             
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

答案 1 :(得分:8)

正如其他人所解释的那样,这是因为Python的窥视孔优化器可以优化算术运算而不是比较。

为Basic编译器编写了自己的窥孔优化器后,我可以向您保证,优化常量比较与优化常量算术运算一样简单。所以没有技术上的理由说明为什么Python应该做后者而不是前者。

然而,每个这样的优化都必须单独编程,并且需要两个成本:编程它的时间,以及占用Python可执行文件空间的额外优化代码。因此,您发现自己必须进行一些分类:哪些优化足以使其值得花费?

似乎Python的实现者,合理地决定首先优化算术运算。也许他们将在未来的版本中进行比较。

答案 2 :(得分:4)

快速disassambling表明比较涉及更多操作。根据{{​​3}},this answer"peephole optimiser")对乘法,加法等进行了一些预先计算,但对于比较运算符则没有:

>>> import dis
>>> def a():
...   return 2*3
... 
>>> dis.dis(a)
  2           0 LOAD_CONST               3 (6)
              3 RETURN_VALUE
>>> def b():
...   return 2 < 3
... 
>>> dis.dis(b)
  2           0 LOAD_CONST               1 (2)
              3 LOAD_CONST               2 (3)
              6 COMPARE_OP               0 (<)
              9 RETURN_VALUE

答案 3 :(得分:4)

与其他人评论一样 - 这是由于窥视孔优化器预先计算了2 * 3(6)的结果。正如展示

0 LOAD_CONST               3 (6)

但试试这个 - 它会禁用优化器预先计算结果

>>> def a(a, b):
...     return a*b
...
>>> dis.dis(a)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_MULTIPLY
              7 RETURN_VALUE
>>> def c(a,b):
...     return a<b
...
>>> dis.dis(c)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 COMPARE_OP               0 (<)
              9 RETURN_VALUE
>>>

如果你计算这些功能,比较会更快。

答案 4 :(得分:3)

对于python案例,上述答案是正确的。对于机器代码来说,事情要复杂一些。我假设我们在这里讨论整数运算,浮点数和复杂对象都不适用。此外,我们假设您正在比较的值已经加载到寄存器中。如果他们没有从任何地方获取它们可能比实际操作长100倍

现代CPU有几种比较两个数字的方法。非常受欢迎的是做XOR a,b如果你只想看两个值是否相等或CMP a,b如果你想知道值之间的关系(更少,更大,相等等)。 CMP操作只是一个减法,结果被抛弃,因为我们只对post-op标志感兴趣。

这两个操作都是深度1,因此可以在单个CPU周期中执行。这是你能尽可能快的。乘法是重复加法的一种形式,因此操作的深度通常等于寄存器的大小。可以通过一些优化来减少深度,但通常乘法是CPU可以执行的较慢操作之一。

然而,乘以0,1或任何2的幂可以减少到移位操作。这也是深度一次操作。因此比较两个数字需要相同的时间。考虑十进制系统,您可以通过在数字末尾附加零来将任意数字乘以10,100,1000。任何优化编译器都会识别这种类型的乘法,并使用最有效的运算。现代CPU也非常先进,因此它们可以通过计算任何操作数中设置的位数来在硬件中执行相同的优化。如果它只是一位,则操作将减少到移位。

因此,在您的情况下,乘以2与比较两个数字一样快。正如上面的人指出的那样,任何优化编译器都会看到你将两个常数相乘,所以它将替换只需用函数返回一个常量。

答案 5 :(得分:1)

哇,@ mu的回答引起了我的注意!但是,在得出结论时不要一概而论是重要的......您正在检查常数而不是变量的时间。对于变量,乘法似乎比比较慢。

这是一个更有趣的案例,其中要比较的数字存储在实际变量中......

import timeit
def go():
    number=1000000000
    print
    print 'a>b, internal:',timeit.timeit(setup="a=1;b=1", stmt="a>b", number=number)
    print 'a*b, internal:',timeit.timeit(setup="a=1;b=1", stmt="a*b", number=number)
    print 'a>b, shell   :',
    %%timeit -n 1000000000 "a=1;b=1" "a>b"
    print 'a*b, shell   :',
    %%timeit -n 1000000000 "a=1;b=1" "a*b"
go()

结果给出:

a>b, internal: 51.9467676445
a*b, internal: 63.870462403
a>b, shell   :1000000000 loops, best of 3: 19.8 ns per loop
a>b, shell   :1000000000 loops, best of 3: 19.9 ns per loop

在宇宙中恢复秩序;)

为了完整性,让我们看一些更多的案例......如果我们有一个变量和一个常量呢?

import timeit
def go():
    print 'a>2, shell   :',
    %%timeit -n 10000000 "a=42" "a>2"
    print 'a*2, shell   :',
    %%timeit -n 10000000 "a=42" "a*2"
go()

a>2, shell   :10000000 loops, best of 3: 18.3 ns per loop
a*2, shell   :10000000 loops, best of 3: 19.3 ns per loop

bools会发生什么?

import timeit
def go():
    print 
    number=1000000000
    print 'a==b    : ', timeit.timeit(setup="a=True;b=False",stmt="a==b",number=number) 
    print 'a and b : ', timeit.timeit(setup="a=True;b=False",stmt="a and b",number=number) 
    print 'boolean ==, shell   :',
    %%timeit -n 1000000000 "a=True;b=False" "a == b"
    print 'boolean and, shell   :',
    %%timeit -n 1000000000 "a=False;b=False" "a and b"
go()

a==b    :  70.8013108982
a and b :  38.0614485665
boolean ==, shell   :1000000000 loops, best of 3: 17.7 ns per loop
boolean and, shell   :1000000000 loops, best of 3: 16.4 ns per loop

:D 现在这很有趣,似乎布尔值,比==快。然而所有这一切都可以,因为Donald Knuth不会失眠,最好的比较方法就是使用和......

在实践中,我们应该检查numpy,这可能更重要......

import timeit
def go():
    number=1000000 # change if you are in a hurry/ want to be more certain....
    print '====   int   ===='
    print 'a>b  : ', timeit.timeit(setup="a=1;b=2",stmt="a>b",number=number*100) 
    print 'a*b  : ', timeit.timeit(setup="a=1;b=2",stmt="a*b",number=number*100) 
    setup = "import numpy as np;a=np.arange(0,100);b=np.arange(100,0,-1);"
    print 'np: a>b  : ', timeit.timeit(setup=setup,stmt="a>b",number=number) 
    print 'np: a*b  : ', timeit.timeit(setup=setup,stmt="a*b",number=number) 
    print '====   float ===='
    print 'float a>b  : ', timeit.timeit(setup="a=1.1;b=2.3",stmt="a>b",number=number*100) 
    print 'float a*b  : ', timeit.timeit(setup="a=1.1;b=2.3",stmt="a*b",number=number*100) 
    setup = "import numpy as np;a=np.arange(0,100,dtype=float);b=np.arange(100,0,-1,dtype=float);"
    print 'np float a>b  : ', timeit.timeit(setup=setup,stmt="a>b",number=number) 
    print 'np float a*b  : ', timeit.timeit(setup=setup,stmt="a*b",number=number) 
    print '====   bool ===='
    print 'a==b    : ', timeit.timeit(setup="a=True;b=False",stmt="a==b",number=number*1000) 
    print 'a and b : ', timeit.timeit(setup="a=True;b=False",stmt="a and b",number=number*1000) 
    setup = "import numpy as np;a=np.arange(0,100)>50;b=np.arange(100,0,-1)>50;"
    print 'np a == b  : ', timeit.timeit(setup=setup,stmt="a == b",number=number) 
    print 'np a and b : ', timeit.timeit(setup=setup,stmt="np.logical_and(a,b)",number=number) 
    print 'np a == True  : ', timeit.timeit(setup=setup,stmt="a == True",number=number) 
    print 'np a and True : ', timeit.timeit(setup=setup,stmt="np.logical_and(a,True)",number=number) 
go()

====   int   ====
a>b  :  4.5121130192
a*b  :  5.62955748632
np: a>b  :  0.763992986986
np: a*b  :  0.723006032235
====   float ====
float a>b  :  6.39567713272
float a*b  :  5.62149055215
np float a>b  :  0.697037433398
np float a*b  :  0.847941712765
====   bool ====
a==b  :  6.91458288689
a and b  :  3.6289697892
np a == b  :  0.789666454087
np a and b :  0.724517620007
np a == True  :  1.55066706189
np a and True :  1.44293071804

同样,同样的行为...... 所以我想,一般来说,通过使用==,

可以获益

至少在Python 2中(Python 2.7.11 | Anaconda 2.4.1(64位)|(默认,2016年2月16日,09:58:36)[MSC v.1500 64 bit(AMD64)]),在哪里我尝试了所有这些...