按位旋转是否比当前Intel CPU的变换慢?

如果java.lang.Integer.rotateLeft通过使用旋转指令进行优化并为其编写基准,我很好奇。 结果是不确定的:它比两个class次快得多,但比一个class次慢一点。 所以我用C ++重写了它并获得了相同的结果。 通过g++ -S -Wall -O3编译时,我可以看到生成的汇编器中的指令。 我的CPU是Intel Core i5。

基准测试很长,肯定不是最好的代码,但我认为它没有被破坏。 或者是吗? 根据文件记录,轮换需要一个周期,就像轮class一样。 谁能解释一下结果呢?

 rotations: 6860 shift: 5100 

前两个答案是错误的。 gcc和java的JIT都知道旋转指令并使用它们。 关于gcc看到上面的链接,关于java看看我的java基准测试及其结果

 benchmark ns linear runtime Rotate 3.48 ==================== NonRotate 5.05 ============================== Shift 2.16 ============ 

我不知道gcc和java jit能够识别出一系列SHIFT和OR运算符可以简化为ROTATE指令,非常有趣。

g ++编译器展开你的循环并使用SHIFT immediateROTATE immediate指令(因为你按常数值移动和旋转)。

这是在TimeShift循环展开案例中重复的六个指令序列:

 movq %rax, %rbx salq $13, %rbx leaq (%rbp,%rbx), %rbx movq %rdi, %rbp sarq $27, %rbp xorq %rbx, %rdx 

这是在TimeRotate循环展开案例中重复的六个指令序列:

 movq %rdx, %rbx rorq $45, %rbx leaq (%rbp,%rbx), %rbx movq %r8, %rbp rorq $49, %rbp xorq %rbx, %r9 

它们的区别主要在于使用salq / sarq进行SHIFT和rorq进行ROTATE所以你想知道为什么时间不同。

答案深入于Sandy Bridge的微架构(您的Core i5处理器),可在INTEL®64和IA-32处理器架构优化参考手册中找到最新Order Number: 248966-026 April 2012

无论您使用by 1操作码还是by immediate操作, SHIFT指令都有1个周期延迟。 它可以从Port 0Port 1 ,因此具有0.5个周期的吞吐量 – 处理器可以在每个周期发送和退出两个SHIFT immediate指令。 如果需要条件标志的结果(它们不在gcc生成的代码中),则ROTATE指令需要三个微操作,否则需要两个微操作(在您的情况下为两个微操作)。 但是, ROTATE指令只能从Port 1调度,因此具有1个周期的吞吐量 – 处理器每个周期只能调度和退出一个ROTATE immediate

我复制了下面的相关图片和部分。

3.5.1.5按位旋转

按位旋转可以在CL寄存器中指定的旋转,立即常数和1位之间进行选择。 通常,通过立即旋转和通过寄存器指令旋转比旋转1位慢。 rotate by 1指令具有与shift相同的延迟。 汇编/编译器编码规则35.(ML影响,L一般性)避免通过寄存器进行ROTATE或通过立即指令进行ROTATE。 如果可能,请使用ROTATE by 1指令替换。 在Intel微体系结构代码名称Sandy Bridge中,ROL / ROR by立即具有1个周期的吞吐量,SHLD / SHRD使用相同的寄存器作为源和目标通过立即常量具有1个周期的延迟,具有0.5个周期的吞吐量。 “ROL / ROR reg,imm8”指令有两个微操作,旋转寄存器结果的延迟为1个周期,标志为2个周期(如果使用的话)。 在英特尔微体系结构代码名称Ivy Bridge中,当使用溢出标志结果时,立即大于1的“ROL / ROR reg,imm8”指令是一个具有一个周期延迟的微操作。 当立即数为1时,后续指令对ROL / ROR的溢出标志结果的依赖将看到具有双周期延迟的ROL / ROR指令。

2.4.4.2执行单元和发布端口

在每个周期,核心可以将μop发送到四个发布端口中的一个或多个。 在微体系结构级别,存储操作进一步分为两部分:存储数据和存储地址操作。 通过其将μops分派给执行单元以及加载和存储操作的四个端口如图2-6所示。 一些端口每个时钟可以发送两个μop。 这些执行单元标记为Double Speed。

端口0.在周期的前半部分,端口0可以调度一个浮点移动μop(浮点堆栈移动,浮点交换或浮点存储数据)或一个算术逻辑单元(ALU)μop (算术,逻辑,分支或存储数据)。 在周期的后半部分,它可以调度一个类似的ALUμop。

端口1.在周期的前半部分,端口1可以调度一个浮点执行(除了移动,所有SIMD操作之外的所有浮点运算)μop或一个正常速度整数(乘法,移位和旋转)μop或一个ALU(算术)μop。 在周期的后半部分,它可以调度一个类似的ALUμop。

端口2.此端口支持每个周期分配一个加载操作。

端口3.此端口支持每个周期分配一个存储地址操作。

每个周期的总发布带宽范围为0到6μs。 每个管道包含几个执行单元。 μops被分派到与正确操作类型对应的管道。 例如,整数算术逻辑单元和浮点执行单元(加法器,乘法器和除法器)可以共享管道。

图2-11。无序核心中的执行单元和端口

根据这个基准测试 ,移位和旋转两者在CPU上具有相同的延迟,但是旋转具有较低的吞吐量(在那里列出的结果为“T”是相互吞吐量,这更容易与延迟相比)。 这可能正是你所看到的那种结果 – 较低的吞吐量类型会稍微减少,但是你并没有完全使执行单元饱和,因此它没有显示出2的完全因素差异。 测试你自己并不容易 ,特别是如果你必须打击编译器使它发出你想要的指令。

当您查看微基准时,您必须考虑JIT将优化常见模式,例如移位,它比非常见模式更有效地识别,例如旋转(或它无法识别的模式)这可能意味着两个操作应该采取相同的时间可以完全不同,因为一个比另一个更加优化。 例如,更多的循环展开或死代码删除。

即使是简单的指令也可以相互作用产生不同的意外结果 换句话说,你不能看一条指令,并假设它告诉你在使用更多指令时它将如何执行。 语境在如此低的水平上很重要。

我建议你尝试在一个现实的程序中比较这些操作,我怀疑你很难找到可衡量的差异。