【问题标题】:how do i rotate a value in assembly我如何在汇编中旋转一个值
【发布时间】:2017-11-20 16:44:10
【问题描述】:

我正在用 x86 64 位汇编实现一个函数,我无法更改:

   unsigned long rotate(unsigned long val, unsigned long num, unsigned long direction); 

direction- 1 为左,0 为右。

这是我向左移动的代码,但它不起作用最后一位已关闭。有人能帮助我吗。

  rotate: 
  push rbp 
  push rdi
  push rsi
  push rdx
  mov rbp, rsp 
  sub rsp, 16
  cmp rdx, 1
  je shift_left


shift_left: 
   mov rax, rdi
   shl rax, cl
   mov rax, rax
   mov rcx, rdi
   sub cl, 64
   shl rcx, cl 
   or rax rdx
   mov rax, rax
   add rsp, 16
   #I pop all the registers used and ret

【问题讨论】:

  • 请显示您的输入、您期望的结果以及您得到的结果。请务必使用几种不同的输入进行测试,以查看错误中的模式。
  • 您使用的是哪个 ABI?我的意思是,您的参数在哪些寄存器中?
  • 你平台上的unsigned longuint64_t一样吗?如果移位计数num 超过val 的操作数宽度,您期望什么行为?接口是固定的,还是可以修改的?特别是,为什么要分开方向和班次计数?例如,Fortran 使用的约定是负移位计数表示“右移”,而正移位计数表示“左移”。这有助于高效实施。

标签: assembly bit-manipulation x86-64


【解决方案1】:

x86 有旋转指令。使用rol rax, cl 向左旋转,ror rax, cl 向右旋转。

您似乎没有意识到clrcx / 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 上从 directioncl 只有 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 上的分支非常可预测,那么分支实现实际上会更快。但通常这意味着最好只内联 rolror 指令。


或者这个: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,clrdi 结果独立于前面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

唯一可以提高效率的方法是内联到调用者以避免movcall/ret 指令。 (或者对于恒定计数旋转,使用立即旋转计数,使其成为 Intel CPU 上的单微指令。)

【讨论】:

    猜你喜欢
    • 2021-01-21
    • 1970-01-01
    • 2018-04-02
    • 2011-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多