我想用英特尔I64汇编程序做一些长整数数学运算(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而不是NOT上使用NEG指令时,它对我来说是“+1”但是Carry错了,NEG RAX在RAX为零时清除了Carry,但是我需要携带JUST IN CASS。因此,下一个最佳方法可能是(4条指令和11个字节的代码):
NOT RDX
NEG RAX
CMC
ADC RDX,0 ; fixed, thanks lurker
还有4条指令。但是不是加+1,我可以减去-1,因为SBB将Carry-Bit加到减数上,当Carry清零时我会加+1。所以我的下一个最好的尝试就是这个,有3个指令和10个字节的代码:
NOT RDX
NEG RAX
SBB RDX,-1
从我冗长的文字中可以看出,这一点并不明显。在汇编程序中使用级联2的补码是否有更好,更易理解的方法?
答案 0 :(得分:2)
更短的指令或更少的指令并不一定意味着更快的执行,因为每条指令的延迟和吞吐量都不同
例如enter
,dad
,loop
等过时的说明会非常慢,而且它们只是为了向后兼容。甚至是inc
is sometimes slower than add
。与上面在某些μarchs上使用的cmc
相同
因此,可以并行执行的更长系列的低延迟指令将更快地运行。一些常见的指令组甚至可以融合在一起形成一个宏操作。编译器的优化器总是知道这一点,并会选择最合适的指令发布。
对于此代码段
__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
前两个xor指令的成本零μ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, reg
,mov
can also be "free"
如果你优化空间(例如在某些高速缓存未命中的情况下),情况可能会有所不同,因为一些即时和指令很长
是否更快或者不需要一些微观基准测试。但在英特尔CPU上,英特尔编译器(ICC)通常比其他编译器具有更高的性能,因为它更好地理解架构。
请注意,该操作称为否定,而不是两个补码,这是一种编码负数的方法
答案 1 :(得分:0)
BTW,在32位或16位模式下,使用EDX:EAX或DX:AX取反2寄存器号是相同的。使用相同的指令序列。
要复制和求反,@ phuclv的答案显示有效的编译器输出。最好的选择是将目的地异或归零,然后使用sub
/ sbb
。
对于AMD,英特尔Broadwell及更高版本的前端,这是4微秒。在Broadwell之前的Intel上,sbb reg,reg
是2 oups。异或归零不在关键路径上(可能在要取反的数据准备好之前发生),因此对于上半部分,总延迟为2或3个周期。下半部分当然具有1个周期的延迟。
在Ryzen上,低半部的Clang mov/neg
可能更好,Ryzen具有消除GP整数的功能,但是仍然需要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
,就设置标志和性能而言。
ADC/SBB with an immediate 0
is only 1 uop on Intel SnB/IvB/Haswell,作为特殊情况。不过,在Nehalem和更早版本上仍然只有2微秒。但是如果没有移动消除,mov
到另一个寄存器然后sbb
回到RDX会更慢。
低半部分(在RAX中)在准备好作为neg
的输入之后的第一个周期中准备就绪。 (因此,可以使用下半部分开始无序执行更高版本的代码。)
上半部分neg rdx
可以与下半部分并行运行。然后sbb rdx,0
必须等待rdx
的{{1}}和neg rdx
的CF。因此,它在下半部分之后的1个周期中的较晚时刻准备就绪,或者在输入的上半部分准备就绪后的2个周期中的较晚时刻准备就绪。
以上序列比任何问题都要好,因为在非常常见的Intel CPU上的uops更少。在Broadwell及更高版本上(单双neg rax
,而不仅仅是立即数0)
SBB
这四个指令序列中的任何一个显然都不是最优的,它们的总运算量更大。而且其中一些具有较差的ILP /依赖链/延迟,例如,下半部分的关键路径上有2条指令,上半部分的关键路径上有3个周期链。