x << 1或x << 10哪个更快?


83

我发誓,我不想优化任何事情,我只是出于好奇而问这个问题。我知道,在大多数硬件有位移(例如的组件的命令shlshr),它是一个命令。但这有多少关系(纳秒级或CPU精巧度)重要?换句话说,以下任一处理器在任何CPU上的运行速度都更快吗?

x << 1;

x << 10;

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


17
天哪,我看了一下代码,我的第一个念头是“流式印刷操作员”。我需要休息一下。
科斯

4
我想我听到有人在他们的脑海中淡淡地说“过早的优化”,或者只是我的想象力。
TIA

5
@tia他说他不会优化任何东西:)

1
@Grigory是的,这就是为什么我们在这里看不到有人跳过带有该短语的问题的原因。:D
tia

1
附带说明:我最近认识到向左移动和向右移动不一定会消耗相同的CPU时间。就我而言,右移要慢得多。首先我很惊讶,但我认为答案是左移手段的逻辑和右也许换档装置算术:stackoverflow.com/questions/141525/...
基督教阿默尔

Answers:


84

可能取决于CPU。

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

所以最重要的是……不。没有不同。


21
太好了,现在我有一个图像告诉我的CPU做一个桶状滚动卡在我的头上……
Ignacio Vazquez-Abrams 2010年

11
错误-非常取决于处理器。在某些处理器上,这是恒定时间。在其他情况下,每个移位可能是一个周期(我曾经使用一个移位约60,000个位来作为测量处理器时钟速度的软件方式)。在其他处理器上,可能仅存在用于单位移位的指令,在这种情况下,将多位移位委派给位于循环中的库例程。
quick_now 2010年

4
@quickly_now:那肯定是衡量时钟速度的一种不好的方法。没有一个处理器愚蠢到实际上不能执行60,000个转换。只需将其转换为即可60000 mod register_size。例如,一个32位处理器将只使用移位计数的5个最低有效位。
卡萨布兰卡2010年

4
Inmos晶片机具有一个移位运算符,该移位运算符的移位次数为32位操作数。如果需要,您可以进行40亿个班次,每个班次1个时钟。“没有处理器足够愚蠢”。不好意思,错了。这个做了。但是,您DID需要在汇编器中对该部分进行编码。编译器进行了明智的修改/优化(只需将结果设置为0,什么也不做)。
quick_now 2010年

5
遗憾的是,奔腾4丢失了桶形移位器,这导致其总体上不佳的每时钟指令率。我认为Core Blah架构可以解决问题。
罗素·博罗戈夫

64

一些嵌入式处理器仅具有“移位1”指令。在此类处理器上,编译器将x << 3变为((x << 1) << 1) << 1

我认为摩托罗拉MC68HCxx是受此限制的最受欢迎的系列之一。幸运的是,这种架构现在很少见,大多数现在都包括具有可变移位大小的桶形移位器。

具有许多现代派生功能的英特尔8051也无法移位任意位数。


12
在嵌入式微控制器上仍然很常见。
本杰克逊

4
在“稀有”下是什么意思?根据统计,出售的8位微控制器的数量大于所有其他类型的MPU的数量。
Vovanium

当您以相同的单价获得16位(例如TI的MSP430)具有更多程序ROM,更多工作RAM和更多功能时,8位微控制器在新开发中的使用就不多了。甚至某些8位微控制器都具有桶形移位器。
Ben Voigt 2010年

1
微控制器的字长与是否具有桶形移位器无关,我提到的MC68HCxx系列也具有16位处理器,所有这些处理器一次只能移位一个位。
Ben Voigt 2010年

事实是,大多数8位MCU都没有桶形移位器,尽管您说对了,这是不对的,并且有没有桶形移位器的非8位。对于没有[桶形移位器]的机器,位数可以作为可靠的近似值。同样事实是,MCU的CPU内核通常不会选择型号,而是使用片上外围设备。通常以相同的价格为更丰富的外围设备选择8位。
Vovanium

29

有很多情况。

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

  2. 如果MPU仅具有1个移位x << 10,通常会比较慢,因为通常是10个移位或2个字节的字节复制。

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

  4. 除第3种情况外,编译器可能会关心有效位的数量,x << 10并将进一步的操作优化为较小宽度的操作,例如将16x16乘法替换为16x8 1(因为低字节始终为零)。

注意,有些微控制器根本没有左移指令,add x,x而是使用了。


我不明白,为什么x << 10快于x << 8,所以在x << 8中,您需要从16位的低位字节开始加载,而不需要加载两次移位。我不明白。
无2010年

3
@无:我没有说x << 10比x << 8快。
Vovanium

9

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


1
指令是否以相同的周期数执行?在某些体系结构上,同一条指令将根据操作数转换为一些不同的操作码,并且需要1到5个周期。
尼克T

@尼克ARM指令通常需要1或2个周期。不确定使用新的体系结构。
onemasse 2010年

2
@Nick T:他谈到ARM时,转移并不是作为专用指令,而是作为许多数据处理指令的“功能”。即,ADD R0, R1, R2 ASL #3将R1和R2左移3位。
Vovanium


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 << E2的结果是E1左移E2位的位置;空位用零填充。如果E1具有无符号类型,则将结果的值乘以E1乘以2,再乘以幂E2,如果E1具有无符号长类型,则将结果取ULONG_MAX + 1为模,否则为UINT_MAX + 1。(常量ULONG_MAX和UINT_MAX在标头中定义。)

E1 >> E2的结果是E1右移E2位的位置。如果E1具有无符号类型,或者E1具有带符号类型和非负值,则结果的值是E1的商的整数部分除以数量2的幂次幂。如果E1具有带符号的类型和负值,则结果值是实现定义的。

所以:

x = y << z;

“ <<”:y×2 z(如果发生溢出则不确定);

x = y >> z;

“ >>”:为符号定义的实现方式定义(通常是算术移位的结果:y / 2 z)。


我认为1u << 100不是UB。这仅仅是0
阿尔钦Tsirunyan

@Armen Tsirunyan:1u << 100一点点的移位可能是溢出;1u << 100因为算术移位为0。在ANSI C下,<<是一个移位。zh.wikipedia.org/wiki/Arithmetic_shift

2
@Armen Tsirunyan:请参见ANSI第3.3.7节-如果右操作数的值为负或大于或等于提升后的左操作数的位宽度,则行为不确定。因此,您的示例是任何ANSI C系统上的UB,除非有101+位类型。
狼,2010年

@ carrot-pot:好的,您说服了我:)
Armen Tsirunyan 2010年

相关:x << (y & 31)如果编译器知道目标体系结构的移位指令掩盖了计数,则仍然可以编译为没有AND指令的单个移位指令(就像x86一样)。(最好不要对掩码进行硬编码;从掩码中获取CHAR_BIT * sizeof(x) - 1或从掩码中获取。)这对于编写旋转惯用法时非常有用,无论输入如何,该惯用法都可以编译成一条指令而无需任何C UB。(stackoverflow.com/questions/776508/…)。
彼得·科德斯

7

可以想象,在8位处理器上,x<<1实际上可能比16位值得多x<<10

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

byte1 = (byte1 << 1) | (byte2 >> 7)
byte2 = (byte2 << 1)

x<<10会更简单:

byte1 = (byte2 << 2)
byte2 = 0

请注意,x<<1移位比移位更频繁,甚至更远x<<10。此外,的结果x<<10不取决于字节1的内容。这可以另外加快操作速度。


5

在几代Intel CPU(P2或P3?不是AMD,如果我没记错的话)上,移位操作的速度简直太慢了。尽管按位移位1位应该总是很快的,因为它只能使用加法。要考虑的另一个问题是,固定位数的移位是否比可变长度的移位快。即使操作码的速度相同,在x86上,移位的非恒定右手操作数也必须占用CL寄存器,这对寄存器分配施加了额外的约束,并且也可能以这种方式降低程序速度。


1
那就是奔腾4。PPro派生的CPU(如P2和P3)具有快速变化的能力。是的,在x86可变数量的变化是慢于他们可以,除非你可以使用BMI2 shlx/ shrx/ sarx(Haswell的后来和Ryzen)。CISC语义(如果count = 0,则标志未修改)在这里伤害了x86。 shl r32, cl在Sandybridge系列中为3 oups(尽管Intel声称如果未使用标志结果,它可以取消其中一个uops)。AMD具有单码率shl r32, cl(但对于扩展精度而言,则是慢速双移位shld r32, r32, cl
Peter Cordes

1
班次(即使是可变计数)在P6-系列中只是一个单例,但读取标志结果shl r32, cl或立即数不是1的标志结果会使前端停滞,直到班次退休!(stackoverflow.com/questions/36510095/…)。编译器知道这一点,并使用单独的test指令而不是使用移位的标志结果。(但是这种废物上的CPU指令,其中这不是一个问题,请参阅stackoverflow.com/questions/40354978/...
彼得·科德斯

3

与往常一样,它取决于周围的代码上下文:例如,您是否将其x<<1用作数组索引?还是将其添加到其他内容中?在任一种情况下,小移位计数(1或2)可以经常优化甚至超过如果编译器结束有到刚刚移位。更不用说整个吞吐量与延迟,前端瓶颈之间的折衷。微小片段的性能不是一维的。

硬件移位指令不是编译器唯一的编译选项x<<1,但其他答案大多是假设的。


x << 1完全等于x+xunsigned和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,1还是add eax,eax比短1个字节shl eax,10,并且代码大小会直接(解码/前端瓶颈)或间接(L1I代码缓存未命中)影响性能。

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


x仍然需要原始值的情况下,通常需要复制+移位。但是大多数x86整数指令都是就地操作。 (目标是诸如add或的指令的来源之一shl。)x86-64 System V调用约定在寄存器中传递args,第一个arg进入edi并且返回值在eax,因此返回的函数x<<10还使编译器发出copy + shift码。

LEA指令允许您进行移位和相加(移位计数为0到3,因为它使用寻址模式的机器编码)。它将结果放在单独的寄存器中。

gcc和clang都以相同的方式优化了这些功能,就像您在Godbolt编译器资源管理器中看到的那样

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)家庭和推土机/里森(Ryzen)。在Intel上,每时钟吞吐量只有1个,延迟为3c lea eax, [rdi + rsi + 123]。(相关:为什么此C ++代码比我用来测试Collat​​z猜想的手写程序集还要快?在这方面进行了详细介绍。)

无论如何,复制+移位10需要单独的mov指令。在许多最近的CPU上,它的延迟可能为零,但仍占用前端带宽和代码大小。(x86的MOV真的可以“免费”吗?为什么我根本不能复制它?

还相关:如何在x86中仅使用2条连续的leal指令将寄存器乘以37?


编译器还可以自由地转换周围的代码,因此无需进行实际移位,也可以将其与其他操作结合使用

例如,if(x<<1) { }可以使用and来检查除高位以外的所有位。在x86上,你会使用一个test指令一样,test eax, 0x7fffffff/jz .false来代替shl eax,1 / jz。此优化适用于任何班次计数,也适用于大班次缓慢(例如Pentium 4)或不存在(某些微控制器)的机器。

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

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

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.