它可以像任何其他 BigInt 库一样工作,只是速度更快且级别更低:处理器从缓存/RAM 中获取一个数字,添加它,然后再次写回结果。
几乎所有的 CPU都 有这个内置的。您必须围绕相关指令使用软件循环,但如果循环有效,则不会使其变慢。 (由于部分标志停顿,这在 x86 上很重要,见下文)
例如如果 x86 提供 rep adc 来执行 src += dst,以 2 个指针和一个长度作为输入(如 rep movsd 到 memcpy),它仍然会在微码中实现为循环。
32 位 x86 CPU 可能有一个内部实现 rep adc,它在内部使用 64 位加法器,因为 32 位 CPU 可能仍然有一个 64 位加法器。但是,64 位 CPU 可能没有单周期延迟 128b 加法器。所以我不认为有一个特殊的指令会比你可以用软件做的更快,至少在 64 位 CPU 上。
在低功耗、低时钟速度的 CPU 上,一个特殊的宽加法指令可能会很有用,在这种 CPU 上,可以使用具有单周期延迟的真正宽加法器。
您正在寻找的 x86 指令是:
当然,adc 适用于二进制整数,而不是单个十进制数字。 x86 可以在 8、16、32 或 64 位块中adc,这与通常仅在全寄存器宽度下进行 adc 的 RISC CPU 不同。 (GMP calls each chunk a "limb")。 (x86 有一些使用 BCD 或 ASCII 的说明,但这些说明在 x86-64 中被删除了。)
imul / idiv 是签名的等价物。 Add 对有符号 2 的补码的工作方式与无符号的相同,因此没有单独的指令;只是look at the relevant flags to detect signed vs. unsigned overflow。但是对于adc,请记住只有最重要的块有符号位;其余的基本都是未签名的。
ADX 和 BMI/BMI2 添加了一些指令,例如 mulx:全乘而不触及标志,因此它可以与 adc 链交错,从而为超标量 CPU 创建更多指令级并行性以供利用。
在 x86 中,adc 甚至可用于内存目标,因此它的执行方式与您描述的完全一样:一条指令触发 BigInteger 块的整个读取/修改/写入。请参见下面的示例。
大多数高级语言(包括 C/C++)不公开“进位”标志
通常没有直接在 C 中的内部函数 add-with-carry。BigInteger 库通常必须用 asm 编写才能获得良好的性能。
然而,英特尔实际上有defined intrinsics for adc(和adcx/adox)。
unsigned char _addcarry_u64 (unsigned char c_in, unsigned __int64 a, \
unsigned __int64 b, unsigned __int64 * out);
因此,进位结果在 C 中作为unsigned char 处理。对于_addcarryx_u64 内在函数,由编译器来分析依赖链并决定哪些添加与adcx 相关,哪些与@ 相关987654356@,以及如何将它们串起来实现C源代码。
IDK _addcarryx 内在函数的意义是什么,而不是让编译器将 adcx/adox 用于现有的 _addcarry_u64 内在函数,当存在可以利用它的并行 dep 链时。也许有些编译器不够聪明。
这是一个 BigInteger 添加函数的示例,采用 NASM 语法:
;;;;;;;;;;;; UNTESTED ;;;;;;;;;;;;
; C prototype:
; void bigint_add(uint64_t *dst, uint64_t *src, size_t len);
; len is an element-count, not byte-count
global bigint_add
bigint_add: ; AMD64 SysV ABI: dst=rdi, src=rsi, len=rdx
; set up for using dst as an index for src
sub rsi, rdi ; rsi -= dst. So orig_src = rsi + rdi
clc ; CF=0 to set up for the first adc
; alternative: peel the first iteration and use add instead of adc
.loop:
mov rax, [rsi + rdi] ; load from src
adc rax, [rdi] ; <================= ADC with dst
mov [rdi], rax ; store back into dst. This appears to be cheaper than adc [rdi], rax since we're using a non-indexed addressing mode that can micro-fuse
lea rdi, [rdi + 8] ; pointer-increment without clobbering CF
dec rdx ; preserves CF
jnz .loop ; loop while(--len)
ret
在较旧的 CPU 上,尤其是 Sandybridge 之前的 CPU,adc 在 dec 写入其他标志后读取 CF 时会导致部分标志停止。 Looping with a different instruction will help for old CPUs which stall while merging partial-flag writes, but not be worth it on SnB-family.
循环展开对于adc 循环也非常重要。 adc 在 Intel 上解码为多个微指令,因此循环开销是一个问题,尤其是如果您因避免部分标志停顿而有额外的循环开销。如果len 是一个小的已知常数,则完全展开的循环通常是好的。 (例如,编译器只使用add/adc to do a uint128_t on x86-64。)
adc 带有内存目标似乎不是最有效的方法,因为指针差异技巧让我们对 dst 使用单寄存器寻址模式。 (没有那个技巧,memory-operands wouldn't micro-fuse)。
根据 Haswell 和 Skylake 的 Agner Fog's instruction tables,adc r,m 是 2 uops(融合域),每 1 个时钟吞吐量 1 个,而 adc m, r/i 是 4 uops(融合域),每 2 个时钟吞吐量 1 个.显然,Broadwell/Skylake 将 adc r,r/i 作为单 uop 指令运行(利用具有 3 个输入依赖项的 uop 的能力,由 Haswell 为 FMA 引入)并没有帮助。我也不是 100% 确定 Agner 的结果就在这里,因为他没有意识到 SnB 系列 CPU 仅在解码器/uop-cache 中使用微熔丝索引寻址模式,而不是在乱序核心中。
无论如何,这个简单的非展开循环是 6 微指令,并且应该在英特尔 SnB 系列 CPU 上以每 2 个周期运行一次迭代运行。即使部分标志合并需要额外的微指令,这仍然容易少于可以在 2 个周期内发出的 8 个融合域微指令。
一些小的展开可以使每个周期接近 1 adc,因为该部分只有 4 微指令。但是,每个周期 2 次加载和 1 次存储不太可持续。
扩展精度乘法和除法也是可能的,利用扩大/缩小乘法和除法指令。当然,由于乘法的性质,它要复杂得多。
使用 SSE 进行附加进位或 AFAIK 任何其他 BigInteger 操作并没有真正的帮助。
如果您正在设计一个新的指令集,you can do BigInteger adds in vector registers if you have the right instructions to efficiently generate and propagate carry。该线程对在硬件中支持进位标志的成本和收益进行了一些来回讨论,与让软件像 MIPS 那样生成进位:比较以检测无符号环绕,将结果放入另一个整数寄存器中。