Assembly 两个';长整数的s补

Assembly 两个';长整数的s补,assembly,x86-64,micro-optimization,twos-complement,Assembly,X86 64,Micro Optimization,Twos Complement,我想用英特尔I64汇编程序做一些长整数运算(128位),需要创建一个2的补码。假设我的正值是RDX:RAX 2的补码是通过“翻转位并添加1”来完成的。因此,最简单的实现是(4条指令和14个字节的代码): 当我在RAX上使用NEG指令而不是不使用时,它为我执行“+1”,但进位是错误的,NEG-RAX在RAX为零时清除进位,但在这种情况下我需要进位。因此,下一个最好的方法可能是(4条指令和11个字节的代码): 还有4条说明。但是不用加+1,我可以减去-1,因为SBB将进位加在减数上,所以当进位清除时

我想用英特尔I64汇编程序做一些长整数运算(128位),需要创建一个2的补码。假设我的正值是RDX:RAX

2的补码是通过“翻转位并添加1”来完成的。因此,最简单的实现是(4条指令和14个字节的代码):

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

还有4条说明。但是不用加+1,我可以减去-1,因为SBB将进位加在减数上,所以当进位清除时,我将加+1。因此,我的下一个最佳尝试是使用3条指令和10字节代码:

  NOT RAX
  NOT RDX
  ADD RAX,1   ; Can't use INC, it doesn't set Carry
  ADC RDX,0
  NOT RDX
  NEG RAX
  CMC
  ADC RDX,0                  ; fixed, thanks lurker
   NOT RDX
   NEG RAX
   SBB RDX,-1

正如你从我冗长的文本中所看到的,这并不容易理解。有没有更好、更容易理解的方法在汇编程序中进行级联2的补码?

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

例如,过时的指令,如,
dad
。。。将非常缓慢,它们仅用于向后兼容。即使与上面在某些μarch上使用的
cmc
相同

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

对于这个片段

\uuuu int128否定(\uuuu int128 x)
{
返回-x;
}
国际商会19.0.1

前两条异或指令的成本μop,因为。现在您只有两条指令要执行

您可以在上面的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,reg一样

如果您对空间进行优化(比如在某些情况下,缓存未命中率很高),情况可能会有所不同,因为某些即时消息和指令很长

无论它是否更快,都需要一些微观基准测试。但在英特尔CPU上,英特尔编译器(ICC)往往比其他处理器具有更高的性能,因为它更了解体系结构


请注意,该操作称为“求反”,而不是“二补”,这是一种对负数进行编码的方法。顺便说一句,在32位或16位模式下,对2寄存器数求反与EDX:EAX或DX:AX相同。使用相同的指令序列


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

AMD、Intel Broadwell及更高版本的前端为4个UOP。在Broadwell之前的Intel上,
sbb reg,reg
为2 uops。xor归零脱离了关键路径(可能发生在要求反的数据准备就绪之前),因此对于高半部分,总延迟为2或3个周期。当然,下半部分已准备好,具有1个周期的延迟

对于下半部分,Clang的
mov/neg
可能比Ryzen更好,Ryzen对GP integer进行了mov消除,但仍然需要一个ALU执行单元进行异或调零。但对于较旧的CPU,它在延迟的关键路径上放置了一个
mov
。但对于可以使用任何ALU端口的指令,后端ALU压力通常没有前端瓶颈那么大


若要就地求反,请使用
neg
0

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

,作为特例。不过,在Nehalem和更早的版本上仍然有2个UOP。但是如果没有mov消除,
mov
到另一个寄存器,然后
sbb
返回到RDX将会更慢

在准备好作为
neg
的输入后,下半部分(在RAX中)在第一个循环中准备就绪。(因此,可以使用下半部分开始无序执行后续代码。)

高半部
neg rdx
可以与低半部并行运行。然后
sbb-rdx,0
必须等待
neg-rdx的
rdx
neg-rax的CF。因此,它在低半部分后的1个周期或输入高半部分准备就绪后的2个周期中较晚的一个周期准备就绪


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


4指令序列中的任何一个显然都是次优的,总体UOP更大。其中一些具有更差的ILP/依赖链/延迟,如低半部分的关键路径上有2条指令,高半部分的关键路径上有3个周期的指令链。

您似乎认为“更好”等于“更短的代码”,这不必像x86-64那样应用于无序或有序多标量处理器。我想说,最容易理解的实现是第一个实现,如果所有实现都需要相同的时间来执行,我也不会感到惊讶。顺便问一下:您考虑过使用XMM寄存器吗?它们的宽度足以容纳128位的数字,而且(我没有检查)它们可能有整数指令来处理整个数字number@mcleod_ideafix他们没有,所以你仍然有手工搬运的问题。Thanx@lurker,我修好了。是的,我考虑了XMM寄存器。它们是为进位传播的整数向量制作的
neg   rdx              ; high half first
neg   rax             ; subtract RDX:RAX from 0
sbb   rdx, 0          ; with carry from low to high half
;; 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