Assembly 两个';长整数的s补
我想用英特尔I64汇编程序做一些长整数运算(128位),需要创建一个2的补码。假设我的正值是RDX:RAX 2的补码是通过“翻转位并添加1”来完成的。因此,最简单的实现是(4条指令和14个字节的代码): 当我在RAX上使用NEG指令而不是不使用时,它为我执行“+1”,但进位是错误的,NEG-RAX在RAX为零时清除进位,但在这种情况下我需要进位。因此,下一个最好的方法可能是(4条指令和11个字节的代码): 还有4条说明。但是不用加+1,我可以减去-1,因为SBB将进位加在减数上,所以当进位清除时,我将加+1。因此,我的下一个最佳尝试是使用3条指令和10字节代码: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将进位加在减数上,所以当进位清除时
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和更高版本上(单uopSBB
,而不仅仅是立即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