哪个更快:x <1或x <&lt; 10?

时间:2010-11-20 17:54:04

标签: c++ c performance cpu low-level

我不想优化任何东西,我发誓,我只想出于好奇而问这个问题。 我知道在大多数硬件上都有一个比特移位的汇编命令(例如shlshr),这是一个命令。但是,你转移了多少比特(纳秒级,或CPU技巧)是否重要?换句话说,在任何CPU上都是以下哪一种更快?

x << 1;

x << 10;

请不要因为这个问题而讨厌我。 :)

9 个答案:

答案 0 :(得分:83)

可能取决于CPU。

然而,所有现代CPU(x86,ARM)都使用“桶形移位器” - 专门设计用于在恒定时间内执行任意移位的硬件模块。

所以底线是......不。没有区别。

答案 1 :(得分:62)

某些嵌入式处理器只有“逐个移位”指令。在此类处理器上,编译器会将x << 3更改为((x << 1) << 1) << 1

我认为摩托罗拉MC68HCxx是受此限制的更受欢迎的家庭之一。幸运的是,这种架构现在非常罕见,现在大多数都包括一个具有可变移位尺寸的桶形移位器。

英特尔8051具有许多现代衍生产品,也无法移动任意数量的位。

答案 2 :(得分:28)

有很多案例。

  1. 许多高速MPU都有桶形移位器,类似多路复用器的电子电路,可以在恒定时间内进行任何移位。

  2. 如果MPU只有1位移位x << 10通常会更慢,因为它主要通过10次移位或2次移位的字节复制完成。

  3. 但众所周知,x << 10甚至比<{1}}更快。如果x是16位,则只需要低6位(所有其他都将被移出),因此MPU只需加载较低的字节,因此只对8位存储器进行单一访问周期,而x << 1需要两个访问周期。如果访问周期慢于shift(并清除低位字节),x << 10将更快。这可能适用于具有快速板载程序ROM的微控制器,同时访问慢速外部数据RAM。

  4. 作为案例3的补充,编译器可能会关注x << 10中的有效位数,并优化对低宽度位的操作,例如将16x16乘法替换为16x8乘法(因为低位字节始终为零) )。

  5. 注意,有些微控制器根本没有左移指令,而是使用x << 10

答案 3 :(得分:9)

在ARM上,这可以作为另一条指令的副作用来完成。所以潜在地,它们中的任何一个都没有延迟。

答案 4 :(得分:9)

这是my favorite CPU,其中x<<2的时间是x<<1的两倍:)

答案 5 :(得分:7)

这取决于CPU和编译器。即使底层CPU具有桶形移位器的任意位移,这只有在编译器利用该资源时才会发生。

请记住,在C和C ++中,将数据位宽度以外的任何内容移位是“未定义的行为”。签名数据的右移也是“实现定义的”。而不是过分关注速度,关注你在不同的实现上得到相同的答案。

引自ANSI C第3.3.7节:

  

3.3.7按位移位运算符

     

语法

      shift-expression:
              additive-expression
              shift-expression <<  additive-expression
              shift-expression >>  additive-expression
     

约束

     

每个操作数都应具有   积分型。

     

语义

     

整体促销活动   在每个操作数上执行。   结果的类型是   提升左操作数。如果值   右操作数是否定的或是   大于或等于宽度   升级的左操作数的位,   行为未定义。

     

E1的结果&lt;&lt; E2是E1   左移E2位位置;腾空   位用零填充。如果E1有   无符号类型,值的   结果是E1乘以   数量,2增加到功率E2,   如果E1有,则减少模数ULONG_MAX + 1   输入unsigned long,UINT_MAX + 1   除此以外。 (常量ULONG_MAX   和UINT_MAX在标题中定义    。)

     

E1&gt;的结果&gt; E2是E1   右移E2位位置。如果E1   有一个无符号类型或E1有一个   签名类型和非负值,   结果的值是   E1的商的组成部分   除以数量,2提高到   权力E2。如果E1有签名   类型和负值,   结果价值是   实现定义的。

所以:

x = y << z;

“&lt;&lt;”:y×2 z 未定义,如果发生溢出);

x = y >> z;

“&gt;&gt;”:为签名定义的实现(通常是算术移位的结果:y / 2 z )。

答案 6 :(得分:7)

可以想象,在8位处理器上,对于16位值,x<<1实际上比<{em}慢得多

例如x<<10的合理翻译可能是:

x<<1

byte1 = (byte1 << 1) | (byte2 >> 7) byte2 = (byte2 << 1) 会更简单:

x<<10

请注意byte1 = (byte2 << 2) byte2 = 0 如何更频繁地转变,甚至比x<<1更远。此外,x<<10的结果不依赖于byte1的内容。这可以进一步加快操作。

答案 7 :(得分:5)

在几代英特尔CPU上(P2或P3?但不是AMD,如果我没记错的话),比特移位操作速度非常慢。 Bitshift by 1 bit应该总是很快,因为它只能使用加法。要考虑的另一个问题是,通过恒定位数的位移是否比可变长度位移更快。即使操作码的速度相同,在x86上,位移的非常量右手操作数也必须占用CL寄存器,这会对寄存器分配造成额外的限制,并且可能会使程序慢下来。

答案 8 :(得分:3)

与往常一样,它取决于周围的代码上下文:例如你使用x<<1作为数组索引吗?或者将其添加到其他东西?在任何一种情况下,小的移位计数(1或2)通常可以比编译器最终必须移位时更优化。更不用说整个吞吐量与延迟与前端瓶颈之间的权衡。微小片段的表现不是一维的。

硬件转换指令不是编译器编译x<<1的唯一选项,但其他答案主要是假设。

x << 1完全等同于x+x 表示无符号和2的补码有符号整数。编译器总是知道他们在编译时所针对的硬件,因此他们可以利用这样的技巧。

Intel Haswell上,add每时钟吞吐量为4,但立即计数的shl每时钟吞吐量仅为2。 (有关说明表以及http://agner.org/optimize/标记wiki中的其他链接,请参阅)。 SIMD向量移位为每时钟1个(Skylake中为2个),但SIMD向量整数加上每个时钟2个(Skylake中为3个)。但延迟是相同的:1个周期。

还有一个shl的特殊转换编码,其中计数隐含在操作码中。 8086没有立即计数轮班,只有一个和cl注册。这主要与右移相关,因为除非你移动内存操作数,否则你可以添加左移。但如果稍后需要该值,最好先加载到寄存器中。但无论如何,shl eax,1add eax,eaxshl eax,10短一个字节,代码大小可以直接(解码/前端瓶颈)或间接(L1I代码缓存未命中)影响性能。

更一般地说,在x86上的寻址模式下,有时可以将小移位计数优化为缩放索引。目前常用的大多数其他架构都是RISC,并且没有扩展索引寻址模式,但x86是一个通用的架构,值得一提。 (例如,如果要索引一个4字节元素的数组,则int arr[]; arr[x<<1])可以将比例因子增加1。

在仍然需要x的原始值的情况下,需要复制+移位。但大多数x86整数指令就地运行。(目标是addshl等指令的来源之一。)x86-64 System V调用约定通过寄存器中的args,edi中的第一个arg和eax中的返回值,因此返回x<<10的函数也会使编译器发出copy + shift代码。

LEA instruction lets you shift-and-add(移位计数为0到3,因为它使用寻址模式机器编码)。它将结果放在一个单独的寄存器中。

gcc and clang both optimize these functions the same way, as you can see on the Godbolt compiler explorer

int shl1(int x) { return x<<1; }
    lea     eax, [rdi+rdi]   # 1 cycle latency, 1 uop
    ret

int shl2(int x) { return x<<2; }
    lea     eax, [4*rdi]    # longer encoding: needs a disp32 of 0 because there's no base register, only scaled-index.
    ret

int times5(int x) { return x * 5; }
    lea     eax, [rdi + 4*rdi]
    ret

int shl10(int x) { return x<<10; }
    mov     eax, edi         # 1 uop, 0 or 1 cycle latency
    shl     eax, 10          # 1 uop, 1 cycle latency
    ret
具有2个组件的LEA在最近的Intel和AMD CPU上具有1个周期延迟和2个每时钟吞吐量。 (Sandybridge家族和Bulldozer / Ryzen)。在英特尔,lea eax, [rdi + rsi + 123]只有1个时钟吞吐量,3c延迟。 (相关:Why is this C++ code faster than my hand-written assembly for testing the Collatz conjecture?详细介绍了这一点。)

无论如何,复制+移位10需要单独的mov指令。在许多最近的CPU上可能是零延迟,但它仍然需要前端带宽和代码大小。 (Can x86's MOV really be "free"? Why can't I reproduce this at all?

还相关:How to multiply a register by 37 using only 2 consecutive leal instructions in x86?

编译器也可以自由转换周围的代码,因此没有实际的转换,或者它与其他操作结合

例如if(x<<1) { }可以使用and来检查除高位之外的所有位。在x86上,您使用test指令,例如test eax, 0x7fffffff / jz .false而不是shl eax,1 / jz。此优化适用于任何班次计数,它也适用于大计数班次较慢(如奔腾4)或不存在(某些微控制器)的计算机。

许多ISA都有位操作指令,而不仅仅是移位。例如PowerPC有很多位字段提取/插入指令。或者ARM将源操作数的移位作为任何其他指令的一部分。 (因此,移位/旋转指令只是move的一种特殊形式,使用移位源。)

请记住, C不是汇编语言。在调整源代码以便有效编译时,请始终查看优化的编译器输出。