它将像任何其他BigInt库一样工作,只是(快得多)更快且在较低级别上:处理器从高速缓存/RAM中获取一个数字,添加它,然后再写回结果。几乎所有的CPU都内置了这个功能。您必须在相关指令周围使用软件循环,但如果循环有效,则不会使其变慢。(由于部分标志停顿,这在x86上并不容易)。例如,如果x86提供rep adc来执行src += dst,输入2个指针和长度(如memcpy),则仍将实现为微代码中的循环。32位x86 CPU可以有内部实现rep adc,其中64位添加器用于内部计算,因为32位CPU可能仍具有64位加法器。但是,64位CPU可能没有单周期延迟的128b加法器。因此,我不认为拥有特殊指令会比在64位CPU上使用软件获得加速。
也许在低功耗、低时钟速度的CPU上,一种特殊的宽加指令会很有用,其中一个具有单周期延迟的真正宽的加法器是可能的。
您需要的x86指令如下:
当然,
adc
适用于二进制整数,而不是单个十进制数字。 x86可以在8、16、32或64位块中执行
adc
,而RISC CPU通常只能在完整的寄存器宽度上执行adc(
GMP将每个块称为“limb”)。 (x86有一些用于处理BCD或ASCII的指令,但这些指令已被x86-64删除。)
imul
/
idiv
是带符号的等价物。 对于带符号的2的补码,加法与无符号相同,因此没有单独的指令; 只需
查看相关标志以检测有符号和无符号溢出。 但对于
adc
,请记住,只有最高有效块具有符号位; 其余的则必须是无符号的。
ADX和BMI / BMI2添加了一些指令,如
mulx
:完全乘法而不触及标志,因此可以与
adc
链交错使用,以创建更多的指令级并行性,供超标量CPU利用。
在x86中,
adc
甚至可以用于内存目标,因此它的执行方式与您描述的完全相同:一个指令触发了整个BigInteger块的读取/修改/写入。请参见下面的示例。
大多数高级语言(包括C / C ++)不公开“进位”标志
通常在C中没有直接的带进位加法指令。 BigInteger库通常必须用汇编语言编写以获得良好的性能。
然而,英特尔实际上已经定义了adc
指令的内嵌函数(以及adcx
/ adox
)。
unsigned char _addcarry_u64 (unsigned char c_in, unsigned __int64 a, \
unsigned __int64 b, unsigned __int64 * out);
因此,在C语言中,进位结果被处理为一个无符号字符。对于_addcarryx_u64内置函数,由编译器分析依赖链并决定哪些加法使用adcx,哪些使用adox,并如何将它们串联起来实现C源代码。我不知道_addcarryx内置函数的意义是什么,而不是让编译器对现有的_addcarry_u64内置函数使用adcx/adox,当存在可以利用它的并行依赖链时。也许有些编译器不够聪明。
以下是一个使用NASM语法的BigInteger加法函数示例:
global bigint_add
bigint_add:
sub rsi, rdi
clc
.loop:
mov rax, [rsi + rdi]
adc rax, [rdi]
mov [rdi], rax
lea rdi, [rdi + 8]
dec rdx
jnz .loop
ret
在旧的CPU上,特别是Sandybridge之前的CPU中,
adc
会在
dec
写入其他标志后读取CF时导致部分标志停顿。
使用不同指令进行循环将有助于旧CPU合并部分标志写入时的停顿,但在SnB系列上可能不值得。
对于adc
循环,循环展开也非常重要。在Intel上,adc
解码为多个微操作,因此循环开销是一个问题,特别是如果你需要避免部分标志延迟而产生额外的循环开销。如果len
是一个小的已知常数,则完全展开循环通常是不错的选择。(例如,编译器只使用{{link1:add
/adc
来执行x86-64上的uint128_t
。)
adc
指令的内存目标似乎不是最有效的方法,因为指针差异技巧使我们可以使用单寄存器寻址模式来访问目标地址(如果没有这个技巧,
内存操作数将无法微融合)。
根据Haswell和Skylake的
Agner Fog指令表,
adc r,m
是2个uop(融合域),每个时钟周期一个,而
adc m,r/i
是4个uop(融合域),每2个时钟周期一个。显然,Broadwell/Skylake将
adc r,r/i
作为单uop指令运行(利用了Haswell引入的具有3个输入依赖项的uop能力)。我也不能百分之百确定Agner的结果是否正确,因为他没有意识到SnB系列CPU只在解码器/uop-cache中微融合索引寻址模式,而不是在乱序核心中。
无论如何,这个简单的未展开循环有6个微操作,在英特尔SnB系列CPU上应该以每2个周期的迭代一次运行。即使需要一个额外的微操作来进行部分标志合并,仍然比在2个周期中可以发出的8个融合域微操作要少得多。
一些小的展开可以让这个循环接近每个周期1个
adc
,因为那部分只有4个微操作。然而,每个周期的2个加载和一个存储还不太可持续。
扩展精度的乘法和除法也是可能的,利用扩展/缩小乘法和除法指令。当然,由于乘法的本质,这要复杂得多。
对于加法进位,使用SSE并不是很有帮助,或者据我所知,在任何其他BigInteger操作中都不是很有帮助。
如果您正在设计新的指令集,如果您有适当的指令以有效地生成和传播进位,则可以在向量寄存器中执行BigInteger相加。该线程对硬件支持进位标志的成本和收益以及软件生成进位标志(例如MIPS)进行了一些来回讨论:与另一个整数寄存器比较以检测无符号环绕,并将结果放入其中。