【问题标题】:Two's complement of long integer长整数的二进制补码
【发布时间】:2015-06-08 22:32:12
【问题描述】:

我想用 Intel I64 Assembler 做一些长整数数学运算(128 位),并且需要创建一个 2 的补码。假设我的正值在 RDX:RAX 中。

2 的补码是通过“翻转位并加 1”来完成的。所以最天真的实现是(4条指令和14字节代码):

  NOT RAX
  NOT RDX
  ADD RAX,1   ; Can't use INC, it doesn't set Carry
  ADC RDX,0

当我在 RAX 上使用 NEG 指令而不是 NOT 时,它为我执行“+1”但进位错误,当 RAX 为零时 NEG RAX 清除进位,但在这种情况下我需要进位。因此,下一个最佳方法可能是(4 条指令和 11 字节代码):

  NOT RDX
  NEG RAX
  CMC
  ADC RDX,0                  ; fixed, thanks lurker

还有 4 条指令。但是我可以减去 -1 而不是加 +1,因为 SBB 将进位位添加到减数中,所以当进位清除时我会加 +1。所以我的下一个最佳尝试是这个,有 3 条指令和 10 个字节的代码:

   NOT RDX
   NEG RAX
   SBB RDX,-1

从我冗长的文字中可以看出,这并不容易理解。有没有更好、更容易理解的方法在汇编程序中进行级联 2 的补码?

【问题讨论】:

  • 您似乎认为“更好”等于“更短的代码”,这不必像 x86-64 那样应用于乱序多标量处理器。我会说你的实现中最不稳定的是第一个,如果它们都需要相同的时间来执行,我不会感到惊讶。
  • 顺便说一句:您考虑过使用 XMM 寄存器吗?它们足够宽,可以容纳 128 位数字,并且(我还没有检查过)它们可能有整数指令来处理整数
  • @mcleod_ideafix 他们没有,所以你仍然面临手动携带携带的问题。
  • Thanx@lurker,我修好了。是的,我考虑过 XMM 寄存器。它们适用于不能选择进位传播的整数向量。因此,它们可以让您在未检测到的溢出或整数饱和之间进行选择。不适合我的目的。
  • 查看 Agner Fog 的指令表,令人惊讶的是,CMC 在现代微架构上具有低延迟、高吞吐量,因此第二个版本可能具有竞争力。另一方面,考虑依赖链: 1:ADC 通过标志依赖于NOT/ADD,依赖于NOT。 2:ADC 依赖于NOT/CMC 依赖于NEG。 3:SBB 依赖于NOT/NEG。我想说你在上一个版本中找到了一个非常聪明的方法。

标签: assembly x86-64 micro-optimization twos-complement


【解决方案1】:

顺便说一句,在 32 位或 16 位模式下对 2 个寄存器编号取反是相同的,使用 EDX:EAX 或 DX:AX。使用相同的指令序列。


为了复制和否定,@phuclv 的答案显示了高效的编译器输出。最好的办法是对目的地进行异或归零,然后使用sub / sbb

对于 AMD 和 Intel Broadwell 及更高版本的前端而言,这是 4 微指令。在 Broadwell 之前的 Intel 上,sbb reg,reg 是 2 微秒。异或归零不在关键路径上(可能发生在要求反的数据准备好之前),因此高半部分的总延迟为 2 或 3 个周期。低半部分当然准备好了,有 1 个周期延迟。

Clang 的低半部分 mov/neg 在 Ryzen 上可能更好,它对 GP 整数具有 mov-elimination,但仍需要一个 ALU 执行单元来进行异或归零。但对于较旧的 CPU,它将mov 置于延迟的关键路径上。但通常后端 ALU 压力并不像前端瓶颈那么大,对于可以使用任何 ALU 端口的指令。


要就地取反,请使用neg0 中减去

neg   rdx              ; high half first
neg   rax             ; subtract RDX:RAX from 0
sbb   rdx, 0          ; with carry from low to high half

就设置标志和性能而言,neg 完全等同于从 0 开始的 sub

ADC/SBB with an immediate 0 is only 1 uop on Intel SnB/IvB/Haswell,作为特例。不过,Nehalem 和更早的版本仍然是 2 微秒。但是如果没有 mov-elimination,mov 到另一个寄存器然后sbb 回到 RDX 会更慢。

低半部分(在 RAX 中)在准备好作为 neg 的输入后的第一个周期中准备好。 (所以后面代码的乱序执行可以使用低半部分开始。)

高半部分neg rdx 可以与低半部分并行运行。然后sbb rdx,0 必须等待来自neg rdxrdx 和来自neg rax 的CF。所以它在低半后的 1 个周期或输入高半后的 2 个周期中准备好。


上述序列比问题中的任何序列都好,因为在非常常见的 Intel CPU 上微指令更少。在 Broadwell 及更高版本上(单微指令 SBB,而不仅仅是立即 0)

;; equally good on Broadwell/Skylake, and AMD.  But worse on Intel SnB through HSW
NOT RDX
NEG RAX
SBB RDX,-1     ; can't use the imm=0 special case

任何 4 条指令序列中的任何一个显然都不是最佳的,它们的总微指令更多。其中一些具有更差的 ILP / 依赖链 / 延迟,例如低半部分的关键路径上有 2 条指令,或者高半部分的 3 周期链。

【讨论】:

  • 我有一段时间没看这个问题了。很棒的见解,非常感谢您
【解决方案2】:

更短的指令或更少的指令并不一定意味着更快的执行速度,因为每条指令的延迟和吞吐量都不同

例如,enterdadloop... 等过时的指令会非常慢,而且它们的存在只是为了向后兼容。甚至inc is sometimes slower than add。与上面在某些 μarch 上使用的 cmc 相同

因此,可以并行执行的更长系列的低延迟指令将运行得更快。一些常见的指令组甚至可以融合到一个宏操作中。编译器的优化器总是知道这一点,并会选择最合适的指令来发出。

对于这个sn-p

__int128 negate(__int128 x)
{
    return -x;
}

ICC 19.0.1 will generate the following instructions

    xor       edx, edx                                      #3.13
    xor       eax, eax                                      #3.13
    sub       rax, rdi                                      #3.13
    sbb       rdx, rsi                                      #3.13

前两个异或指令花费 μop,因为they're handled at the register-rename stage。现在您只有 2 条指令 可以执行

您可以在上面的 Godbolt 链接中切换编译器,以查看不同编译器(包括 MSVC)否定的各种方法(不幸的是,它还没有 128 位类型)。以下是 GCC 和 Clang 的结果

GCC 8.3:

    mov     rax, rdi
    neg     rax
    mov     rdx, rsi
    adc     rdx, 0
    neg     rdx

叮当声:

    mov     rax, rdi
    xor     edx, edx
    neg     rax
    sbb     rdx, rsi

如您所见,Clang 也仅使用 3 条指令(减去第一个将数据从输入参数移动到必要目的地的指令)。但是像xor reg, regmov can also be "free"

如果您针对空间进行优化(例如在某些高速缓存未命中率较高的情况下),情况可能会有所不同,因为某些立即数和指令很长

它是否更快或不需要一些微基准测试。但在 Intel CPU 上,Intel 编译器 (ICC) 通常能获得比其他编译器更高的性能,因为它更了解架构。

请注意,该操作称为否定,而不是二进制补码,后者是一种编码负数的方法

【讨论】:

  • dad 不是 x86 助记符,aam 等在 x86-64 中无效。根据 Ager Fog 的指令表,cmc 在自 P4 以来的所有 Intel/AMD 微架构上都非常快。
  • 也许我有时应该试试 GCC,至少看看它生成的代码。我使用的是 Visual Studio 2013。
  • gcc.godbolt.org 将帮助您查看几种最常见编译器的汇编输出
猜你喜欢
  • 1970-01-01
  • 2015-08-26
  • 1970-01-01
  • 2013-03-28
  • 1970-01-01
  • 2021-11-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多