<< >>乘法和除法的速度


9

当我对它们进行计时时,可以使用python中的数字<<进行乘法和>>除法,发现使用二进制移位方法比将常规方法除法或乘法快10倍。

为什么使用<<>>*and 要快很多/

什么是现场处理事情,使落后*/这么慢?


2
在所有语言中,不仅是Python,位移都更快。许多处理器具有本机位移指令,将在一个或两个时钟周期内完成该指令。
罗伯特·哈维

4
但是,应记住,通常不推荐使用移位而不是使用普通的除法和乘法运算符,并且会影响可读性。
Azar 2014年

6
@crizly因为充其量是微优化,所以编译器很有可能无论如何都会将其更改为字节码移位。对此有一些例外,例如,当代码对性能至关重要时,但是大多数情况下,您要做的只是使代码变得混乱。
Azar 2014年

7
@Crizly:任何具有出色优化器的编译器都将识别可以通过移位进行的乘法和除法,并生成使用它们的代码。不要丑化您的代码,以求使其优于编译器。
Blrfl 2014年

2
这个关于StackOverflow问题中,对于一个足够小的数字,微基准测试发现Python 3中乘以2的性能比等效的左移稍微好一点。我想我追查的原因是小乘法(当前)与位移位的优化方式不同。只是表明您不能理所当然地根据理论运行更快。
丹·盖茨

Answers:


15

让我们看一下两个小的C程序,它们进行了位移和除法。

#include <stdlib.h>

int main(int argc, char* argv[]) {
        int i = atoi(argv[0]);
        int b = i << 2;
}
#include <stdlib.h>

int main(int argc, char* argv[]) {
        int i = atoi(argv[0]);
        int d = i / 4;
}

然后将它们一起编译gcc -S以查看实际的程序集。

对于移位版本,从调用atoi到返回:

    callq   _atoi
    movl    $0, %ecx
    movl    %eax, -20(%rbp)
    movl    -20(%rbp), %eax
    shll    $2, %eax
    movl    %eax, -24(%rbp)
    movl    %ecx, %eax
    addq    $32, %rsp
    popq    %rbp
    ret

而除法版本:

    callq   _atoi
    movl    $0, %ecx
    movl    $4, %edx
    movl    %eax, -20(%rbp)
    movl    -20(%rbp), %eax
    movl    %edx, -28(%rbp)         ## 4-byte Spill
    cltd
    movl    -28(%rbp), %r8d         ## 4-byte Reload
    idivl   %r8d
    movl    %eax, -24(%rbp)
    movl    %ecx, %eax
    addq    $32, %rsp
    popq    %rbp
    ret

只是看一下,与位移相比,除法版本中还有更多的指令。

关键是他们做什么?

在移位版本中,关键指令是shll $2, %eax逻辑左移-存在除法,其他所有内容都只是在移动值。

在划分版本中,您可以看到idivl %r8d-,但在其上方是一个cltd(将long转换为double)和一些有关溢出和重新加载的附加逻辑。这项额外的工作,通常是必要的,因为我们要处理的是数学而不是位,这对于避免仅通过位数学可能发生的各种错误通常是必要的。

让我们做一些快速乘法:

#include <stdlib.h>

int main(int argc, char* argv[]) {
    int i = atoi(argv[0]);
    int b = i >> 2;
}
#include <stdlib.h>

int main(int argc, char* argv[]) {
    int i = atoi(argv[0]);
    int d = i * 4;
}

与其经历所有这些,不如说一行:

$ diff数位
24c24
> shll $ 2,%eax
---
<sarl $ 2,%eax

在这里,编译器能够识别出可以通过移位完成数学运算,但是不是逻辑移位而是算术移位。如果我们运行这些命令,则它们之间的区别将很明显- sarl保留了符号。这样-2 * 4 = -8虽然shll没有。

让我们在快速的perl脚本中看一下:

#!/usr/bin/perl

$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";

$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";

输出:

16
16
18446744073709551600
-16

嗯... -4 << 218446744073709551600这不正是用乘法和除法打交道时,你可能期望。是正确的,但不是整数乘法。

因此要提防过早的优化。让编译器为您优化-它知道您真正要做什么,并且可能会做得更好,错误更少。


12
这可能是更清晰的配对<< 2* 4>> 2/ 4保持在换档方向上每个实施例中的相同。
Greg Hewgill

5

现有的答案并没有真正解决硬件方面的问题,因此在这个角度上有一点。传统观点认为乘法和除法比移位要慢得多,但是今天的实际情况却更加细微。

例如,可以肯定的是,乘法是在硬件中实现的更复杂的运算,但不一定总是变慢。事实证明,add实现起来也比xor(或一般而言,按位操作)要复杂得多,但是add(和sub)通常会得到足够的晶体管专用于其操作,最终与按位运算符一样快。因此,您不能仅仅将硬件实现的复杂性视为提高速度的指南。

因此,让我们详细了解一下移位与“全”运算符(例如乘法和移位)的比较。

换档

在几乎所有硬件上,以恒定量(即,编译器可以在编译时确定的量)移动很快。特别是,它通常以单个周期的延迟发生,并且每个周期的吞吐量为1或更佳。在某些硬件上(例如某些Intel和ARM芯片),某些常数的移位甚至可能是“免费的”,因为它们可以内置到另一条指令中(lea在Intel上,这是ARM中第一个源代码的特殊移位能力)。

可变量移动更多是灰色区域。在较旧的硬件上,这有时非常慢,并且速度一代一代地改变了。例如,在最初发布的Intel P4上,以可变的数量移动非常缓慢-所需的时间与移动量成正比!在该平台上,使用乘法代替班次可能会有利可图(即,世界已经倒置了)。在以前的Intel芯片以及后续的产品中,变化不那么麻烦。

在当前的Intel芯片上,可变的移动速度不是特别快,但是也不是很糟糕。x86体系结构在进行可变移位时会受阻,因为它们以一种不寻常的方式定义了操作:移位量为0不会修改条件标志,但是所有其他移位都可以。这将阻止标志寄存器的有效重命名,因为直到移位执行后才能确定后续的指令是否应读取由移位写入的条件代码或某些先前的指令,否则无法有效地重命名标志寄存器。此外,移位仅写入部分标志寄存器,这可能导致部分标志停顿。

结果是,在最新的英特尔架构上,可变数量的移位需要三个“微操作”,而大多数其他简单的操作(加,按位运算,甚至乘法)仅需要1个。这种移位最多每2个周期执行一次。

乘法

现代台式机笔记本电脑硬件的趋势是使乘法运算快速进行。实际上,在最近的Intel和AMD芯片上,每个周期都可以进行一次乘法运算(我们称之为互惠吞吐量)。但是,乘法的等待时间为3个周期。因此,这意味着您可以在启动任何3个周期后得到任何给定乘法的结果,但是您可以在每个周期开始一个新的乘法。哪个值(1个周期或3个周期)更重要,取决于算法的结构。如果乘法是关键依赖链的一部分,则延迟很重要。如果不是,则互惠吞吐量或其他因素可能更重要。

他们的主要收获是,在现代笔记本电脑芯片(或更好的笔记本电脑芯片)上,乘法运算是一种快速操作,并且可能比编译器为“获得舍入”以减小强度移位而发出的3或4条指令序列更快。对于可变移位,在Intel上,由于上​​述问题,通常也首选乘法。

在较小的尺寸平台上,乘法可能仍然较慢,因为构建一个完整而快速的32位或特别是64位乘法器会占用大量晶体管和功率。如果有人可以填写最新移动芯片上乘法性能的详细信息,将不胜感激。

划分

从硬件角度来看,除法运算比乘法运算更复杂,并且在实际代码中也很少见,这意味着分配给它的资源可能更少。现代芯片的趋势仍然是朝着更快的分频器发展,但即使是现代的顶级芯片也需要10-40个周期来进行分频,并且它们仅部分流水线化。通常,64位除法甚至比32位除法慢。与大多数其他操作不同,除法可能会根据参数占用可变数量的循环。

如果可以的话,请避免除以移位并替换为移位(或让编译器执行,但可能需要检查汇编)!


2

从算法上来说,BINARY_LSHIFT和BINARY_RSHIFT比BINARY_MULTIPLY和BINARY_FLOOR_DIVIDE更简单,并且可能花费更少的时钟周期。也就是说,如果您有任何二进制数并且需要将N移位,那么您要做的就是将数字移位那么多的空格并替换为零。 二进制乘法通常比较复杂,尽管像Dadda乘法器这样的技术使其运行起来非常快。

当然,当您用2的幂乘/除并用适当的左/右移位代替时,优化的编译器可能会识别出大小写。通过查看反汇编的字节码,python显然不能做到这一点:

>>> dis.dis(lambda x: x*4)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (4)
              6 BINARY_MULTIPLY     
              7 RETURN_VALUE        

>>> dis.dis(lambda x: x<<2)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_LSHIFT       
              7 RETURN_VALUE        


>>> dis.dis(lambda x: x//2)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_FLOOR_DIVIDE 
              7 RETURN_VALUE        

>>> dis.dis(lambda x: x>>1)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 BINARY_RSHIFT       
              7 RETURN_VALUE        

但是,在我的处理器上,我发现乘法和左移/右移具有相似的时序,并且下限除法(以2的幂)大约慢25%:

>>> import timeit

>>> timeit.repeat("z=a + 4", setup="a = 37")
[0.03717184066772461, 0.03291916847229004, 0.03287005424499512]

>>> timeit.repeat("z=a - 4", setup="a = 37")
[0.03534698486328125, 0.03207516670227051, 0.03196907043457031]

>>> timeit.repeat("z=a * 4", setup="a = 37")
[0.04594111442565918, 0.0408930778503418, 0.045324087142944336]

>>> timeit.repeat("z=a // 4", setup="a = 37")
[0.05412912368774414, 0.05091404914855957, 0.04910898208618164]

>>> timeit.repeat("z=a << 2", setup="a = 37")
[0.04751706123352051, 0.04259490966796875, 0.041903018951416016]

>>> timeit.repeat("z=a >> 2", setup="a = 37")
[0.04719185829162598, 0.04201006889343262, 0.042105913162231445]
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.