x86 有旋转指令。使用rol rax, cl 向左旋转,ror rax, cl 向右旋转。
您似乎没有意识到cl 是rcx / ecx 的低字节。因此 shl rcx, cl 正在改变班次计数。您的功能过于复杂,但在您刚刚学习时这很正常。需要练习才能找到可以通过几条指令实现的简单潜在问题。
另外,我认为mov rcx, rdi 应该是mov rcx, rsi。 IDK mov rax,rax 应该是什么;它只是一个空操作。
为左旋转和右旋转调用不同的函数会显着提高效率,除非您实际上需要 direction 作为运行时变量,而不仅仅是构建时间常数 1 或 0。
或者让它无分支,有条件地做cl = 64-cl,因为n的左旋转与64-n的右旋转是一样的。而且因为旋转指令掩盖了计数(无论如何旋转是模块化的),你实际上可以只做-n而不是64-n。 (请参阅 Best practices for circular shift (rotate) operations in C++ 了解使用 -n 而不是 32-n 并编译为单个旋转指令的 C)。
TL:DR 由于旋转对称,您可以通过否定计数来向另一个方向旋转。作为@njuffa points out,您可以编写带有符号移位计数的函数,其中负数表示反向旋转,因此调用者首先会传递给您num 或-num。
请注意,在您的代码中,sub cl, 64 对下一个 shl 的移位计数没有影响,因为 64 位 shl 已经用 cl & 63 掩盖了计数。
我制作了一个 C 版本来看看编译器会做什么(在 the Godbolt compiler explorer 上)。 gcc 有一个有趣的想法:双向旋转并使用cmov 来选择正确的结果。这有点糟糕,因为在英特尔 SnB 系列 CPU 上,可变计数移位/旋转是 3 微秒。 (因为如果计数结果是0,他们必须保持标志不变。参见the shift section of this answer,所有这些都适用于旋转。)
不幸的是,BMI2 只添加了即时计数版本的rorx 和可变计数版本shlx/shrx,而不是可变计数无标志轮换。
无论如何,基于这些想法,这是为 x86-64 System V ABI / 调用约定(允许函数破坏输入参数寄存器和 @ 987654355@/r11)。我假设您在使用 x86-64 SysV ABI(如 Linux 或 OS X)的平台上,因为您似乎在前 3 个参数(或至少尝试),而您的 long 是 64 位。
;; untested
;; rotate(val (rdi), num (rsi), direction (rdx))
rotate:
xor ecx, ecx
sub ecx, esi ; -num
test edx, edx
mov rax, rdi ; put val in the retval register
cmovnz ecx, esi ; cl = direction ? num : -num
rol rax, cl ; works as a rotate-right by 64-num if direction is 0
ret
xor-zero / sub 通常比 mov / neg 好,因为 xor-zeroing 偏离了关键路径。 mov / neg 在 Ryzen 上更好,不过,它具有零延迟整数 mov 并且仍然需要 ALU uop 来进行异或归零。但是,如果 ALU 微指令不是您的瓶颈,这仍然可以。这在 Intel Sandybridge 上是一个明显的胜利(xor-zeroing 与 NOP 一样便宜),并且在其他没有零延迟 mov 的 CPU(如 Silvermont/KNL 或 AMD Bulldozer)上也是一个延迟胜利-家庭)。
cmov 在 Intel 前 Broadwell 上是 2 微秒。一个 2 的补码 bithack 替代 xor/sub/test/cmov 可能同样好,如果不是更好的话。 -num = ~num + 1。
rotate:
dec edx ; convert direction = 0 / 1 into -1 / 0
mov ecx, esi ; couldn't figure out how to avoid this with lea ecx, [rdx-1] or something
xor ecx, edx ; (direction==0) ? ~num : num ; NOT = xor with all-ones
sub ecx, edx ; (direction==0) ? ~num + 1 : num + 0;
; conditional negation using -num = ~num + 1. (subtracting -1 is the same as adding 1)
mov rax, rdi ; put val in the retval register
rol rax, cl ; works as a rotate-right by 64-num if direction is 0
ret
如果内联,这将有更大的优势,因此 num 可能已经在 ecx 中,这使得它比其他选项更短(在代码大小和 uop 计数方面)。
Haswell 的延迟
- 从
direction 准备好到cl 准备好rol:3 个周期(dec / xor / sub)。与其他版本中的test / cmov 相同。 (但在 Broadwell/Skylake test/cmov 上从 direction 到 cl 只有 2 个周期延迟)
- 从
num 准备好到cl 准备好:2 个周期:mov(0) + xor(1) + sub(1),所以num 有空间准备好1个周期后。这比 Haswell 上的 cmov 更好,sub(1) + cmov(2) = 3 个周期。但在 Broadwell/Skylake 上,无论哪种方式都只有 2c。
Broadwell 之前的前端 uop 总数更好,因为我们避免使用 cmov。我们用xor-zeroing 换成了mov,这在 Sandybridge 上更糟,但在其他地方大致相同。 (除了它在num 的关键路径上,这对于没有零延迟的 CPU 很重要mov。)
顺便说一句,如果direction 上的分支非常可预测,那么分支实现实际上会更快。但通常这意味着最好只内联 rol 或 ror 指令。
或者这个:gcc 的输出删除了多余的and ecx, 63。它在某些 CPU 上应该相当不错,但与上述相比没有太大优势。 (而且在包括 Skylake 在内的主流英特尔 Sandybridge 系列 CPU 上显然更糟。)
;; not good on Intel SnB-family
;; rotate(val (rdi), num (rsi), direction (rdx))
rotate:
mov ecx, esi
mov rax, rdi
rol rax, cl ; 3 uops
ror rdi, cl ; false-dependency on flags on Intel SnB-family
test edx, edx ; look at the low 32 bits for 0 / non-0
cmovz rax, rdi ; direction=0 means use the rotate-right result
ret
false 依赖只针对设置标志的 uops;我认为ror rdi,cl 的rdi 结果独立于前面rol rax,cl 的标志合并uop。 (见SHL/SHR r,cl latency is lower than throughput)。但是所有的微指令都需要p0或者p6,所以会存在限制指令级并行的资源冲突。
使用rotate(unsigned long val, int left_count)
调用者在edi 中向您传递一个签名的循环计数。或者,如果你愿意,可以称它为rdi;你忽略了它的低 6 位以外的所有位,实际上你只是在[0, 63 范围内进行左旋转,但这与支持在[-63, +63] 范围内的左右旋转相同。 (较大的值包含在该范围内)。
例如-32 的一个 arg 是 0xffffffe0,它掩盖了 0x20,即 32。在任一方向旋转 32 是相同的操作。
rotate:
mov rax, rdi
mov ecx, esi
rol rax, cl
ret
唯一可以提高效率的方法是内联到调用者以避免mov 和call/ret 指令。 (或者对于恒定计数旋转,使用立即旋转计数,使其成为 Intel CPU 上的单微指令。)