两个补码的长整数

问题描述 投票:7回答:2

我想用英特尔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的补码?

assembly x86-64 micro-optimization twos-complement
2个回答
2
投票

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

例如enterdadloop等过时的指令将非常缓慢,它们仅用于向后兼容。甚至inc is sometimes slower than add。与上面在某些μarchs上使用的cmc相同

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

对于这个片段

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

我喜欢19.0.1 Cakesupoi

will generate the following instructions

前两个xor指令的成本为零μop,因为 xor edx, edx #3.13 xor eax, eax #3.13 sub rax, rdi #3.13 sbb rdx, rsi #3.13 。现在您只有2条指令要执行

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

GCC 8.3:

they're handled at the register-rename stage

锵:

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

如您所见,Clang也只使用3条指令(减去第一条指令将数据从输入参数移动到必要的目标)。但是像 mov rax, rdi xor edx, edx neg rax sbb rdx, rsi xor reg, reg

如果你优化空间(例如在某些高速缓存未命中的情况下),情况可能会有所不同,因为一些immediates和指令很长

无论是更快还是不需要一些微观基准测试。但在英特尔CPU上,英特尔编译器(ICC)通常比其他编译器具有更高的性能,因为它更好地理解架构。

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


0
投票

顺便说一下,在32位或16位模式下,取消2寄存器的数字是相同的,EDX:EAX或DX:AX。使用相同的指令序列。


要复制和否定,@ phuclv的答案显示了高效的编译器输出。最好的办法是将目的地归零,然后使用mov can also be "free" / sub

对于AMD,以及英特尔Broadwell及更高版本的前端,这是4 uops。在Broadwell之前的英特尔,sbb是2 uops。 xor-zeroing不在关键路径上(可能在要被否定的数据准备好之前发生),因此对于高半部分,总延迟为2或3个周期。低半部分当然准备好了1个周期的延迟。

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


要取消就地,使用movneg中减去

0

neg rdx ; high half first neg rax ; subtract RDX:RAX from 0 sbb rdx, 0 ; with carry from low to high half 与0中的neg完全相同,只要设置标志和性能。

sub,作为一个特例。不过,Nehalem和早些时候它仍然是2 uops。但是没有mov-elimination,ADC/SBB with an immediate 0 is only 1 uop on Intel SnB/IvB/Haswell到另一个寄存器然后mov回到RDX会更慢。

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

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


上面的序列比问题中的任何一个更好,在非常常见的Intel CPU上更少的uops。在Broadwell和后来(单uop neg rax,不仅仅是立即0)

SBB

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

© www.soinside.com 2019 - 2024. All rights reserved.