为什么我的处理器没有内置的 BigInt 支持?

10

据我理解,大整数通常在大多数编程语言中被实现为包含数字的数组,例如:当两个大整数相加时,每个数字都像我们在学校里学习的那样一个接一个地相加,例如:

 246
 816
 * *
----
1062

如果*标记表示存在溢出。我是从学校里学到的这个方法,我编写的所有BigInt加法函数都与上面的示例类似。

所以我们都知道,处理器本地只能管理0到2^32/2^64范围内的int。

这意味着,为了实现高级别的大整数算术并提供使用大整数的功能,大多数脚本语言必须实现/使用像上面那样处理整数的BigInt库。

但是,这当然意味着它们将比处理器慢很多。

那么我问自己的问题是:

  • 为什么我的处理器没有内置的BigInt函数?

它会像任何其他BigInt库一样工作,只是(快得多)且在更低的级别上:处理器从缓存/RAM中获取一个数字,对其进行加法运算,然后再次写回结果。

听起来对我来说是个好主意,那么为什么没有类似的东西呢?


5
BigInts并非使用字符串实现,而是使用字节数组实现。但是,如果你将字节数组视为以256为底的字符串表示,则你所说的是正确的。 - JSBձոգչ
9
为什么处理器里没有一个动态的小马和独角兽绘图例程! - Paul Tomblin
1
当 CPU 寄存器只有 8 位宽度时,对于大整数运算的软件库的兴趣更为普遍。那时候进行重要的数学计算唯一的方法就是通过软件库。现在,随着 64 位整数寄存器和硬件浮点数几乎无处不在,使用软件库进行简单的数学计算更多的是出于好奇,而非迫切需要。 - dthorpe
1
一旦你开始进行更大的非固定大小操作(特别是乘除法),就会有许多可能的实现选择,每个选择都有自己的权衡(而且差异很大)。将这些硬编码到处理器中就像使用某个版本的GMP而没有升级或更改选项一样。此外,像GMP这样的库非常庞大,而芯片制造商希望它们的操作相对简单且可验证。 - Realz Slaw
1
@dthorpe 英特尔®架构处理器正在引入新的指令,以实现大整数算术的快速实现。 - Charles Okwuagwu
显示剩余4条评论
8个回答

9

有太多问题需要处理器处理,这些问题都不是它的工作范围。

假设处理器确实具备该功能。我们可以设计一个系统,在其中我们知道给定 BigInt 使用了多少字节 - 只需使用大多数字符串库相同的原则并记录长度。

但是,如果 BigInt 操作的结果超过了预留的空间会发生什么?

有两个选择:

  1. 它将在其拥有的空间内环绕
  2. 或者
  3. 它将使用更多的内存。

问题在于,如果它选择 1),那么它就没有用了 - 你必须事先知道需要多少空间,这也是你想使用 BigInt 的原因之一 - 所以你不会受到这些限制。

如果它选择 2),那么它必须以某种方式分配该内存。内存分配在不同的操作系统中不是以相同的方式进行的,但即使是这样,它仍然必须更新指向旧值的所有指针。它如何知道哪些是指向该值的指针,哪些只是包含与所讨论内存地址相同的值的整数值?


1
处理器确实支持带进位加法和乘法,可以产生完整的低位和高位结果(64b * 64b => 128b)。软件必须在循环中使用它们,但这并不比微码循环慢(例如,如果存在rep adcq来执行src += dst,以2个指针和长度作为输入)。我添加了一个更详细的答案。 - Peter Cordes

8

几乎没有人使用它。 - Michael Myers
2
http://teck.in/year-2016-y2k16-computer-bug-hits-texting-in-us-and-banking-in-australia.html - Paul Tomblin
3
@mmyers:OP并不是在问它是否流行。 :P BCD被用于金融应用中的定点数学,以避免精度损失/舍入误差。 - dthorpe
@dthorpe:是的,但如果BCD并没有被广泛使用,为什么处理器制造商会实现另一种替代方案呢?至少这是我所认为的。 - Michael Myers
2
有趣的是,IEEE-754定义了二进制和十进制基数变量。http://en.wikipedia.org/wiki/IEEE_754-2008。 - user166390
显示剩余2条评论

4
它将像任何其他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加法函数示例:

;;;;;;;;;;;; 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时导致部分标志停顿。使用不同指令进行循环将有助于旧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)进行了一些来回讨论:与另一个整数寄存器比较以检测无符号环绕,并将结果放入其中。


更新:长整数例程能从SSE中受益吗?——如果您安排数据格式以留出一些空余位,允许您延迟规范化,那么是的。 - Peter Cordes

3
假设乘法结果需要三倍的空间(内存)来存储 - 处理器会把结果存储在哪里?使用该结果的用户,包括所有指向它的指针,如何知道其大小突然改变了 - 改变大小可能需要将其重新定位到内存中,因为扩展当前位置会与另一个变量冲突。
这将在处理器、操作系统内存管理和编译器之间创建大量交互,很难使其既通用又高效。
管理应用程序类型的内存不是处理器应该做的事情。

2
据我所知,乘法的结果所需的位数只会等于操作数宽度之和。因此,假设我们正在相乘两个大小相同的变量,你永远不需要比结果空间多超过2倍的空间。 - rmeador
当然可以,但是在运行时你并不知道那个大小,这是一个难点。 - leeeroy
@leeeroy,直到运行时你才会知道几乎任何东西的大小。这有什么难的问题吗?只需计算字节数并分配足够的内存来容纳此操作的结果即可。因为如果你所说的有任何意义,我们就不应该使用计算机,因为在某些时候它们可能无法容纳我们的数据。如今,随着许多GB的内存,你很难用BCD达到极限。 - Pablo Ariel

1

看起来英特尔正在添加(或已经在2015年发布时添加)支持大整数算术的新指令。

在英特尔®架构处理器上引入了新指令,以实现大整数算术的快速实现。大整数算术广泛用于多精度库,用于高性能技术计算,以及用于公钥加密(例如RSA)。在本文中,我们描述了大整数算术所需的关键操作及其使用新指令的高效实现。

http://www.intel.com/content/www/us/en/intelligent-systems/intel-technology/ia-large-integer-arithmetic-paper.html


这是ADX指令集扩展(mulx / adcx / adox),在Broadwell中添加(由CPUID中的ADX功能位表示)。常规的adcmul已经存在;如果您可以同时保持两个带进位加法指令流,并交错使用乘法,这些新指令将简单地增加并行性。这些指令仅使用EFLAGS中的一个位作为其进出位,而不会破坏其他位。 - Peter Cordes

1

我认为,现代处理器不包括bigint支持的主要想法是希望减少ISA并尽可能少地留下指令,这些指令被完全获取、解码和执行。顺便说一句,在x86系列处理器中,有一组指令可以使编写bigint库成为一天的事情。 我认为另一个原因是价格。在晶圆上节省一些空间,放弃可以在更高层次上轻松实现的冗余操作,这样更加高效。


1
我认为处理x86 / x87 / MMX / SSE / SSE2 / SSE3 / SSSE3的处理器并不一定是为了最小化指令集而设计的。 - Jeffrey L Whitledge

0

在 CPU 芯片上,有很多指令和功能争夺空间,最终那些被更频繁使用或被认为更有用的指令将会挤出那些不需要的。实现 BigInt 功能所需的指令已经存在,数学计算也很简单。


-1

BigInt:所需的基本功能是:无符号整数乘法,加上先前的高位。 我用Intel 16位汇编写了一个,然后是32位... C代码通常足够快..即对于BigInt,您可以使用软件库。 CPU(和GPU)设计时未将无符号整数作为最优先考虑。

如果你想编写自己的BigInt...

除法通过Knuths Vol 2完成(这是一堆乘和减,带有一些棘手的补加)

带进位加和减更容易。等等等等

我刚在Intel发布了这个: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx SSE4是否有BigInt库?

i5 2410M处理器我想不能使用AVX [AVX仅适用于非常新的Intel CPU] 但可以使用SSE4.2

是否有适用于SSE的BigInt库? 我猜我在寻找实现无符号整数的东西

PMULUDQ(具有128位操作数) PMULUDQ __m128i _mm_mul_epu32(__m128i a,__m128i b)

并且进行进位。

这是一台笔记本电脑,所以我不能购买一块不太适合无符号整数的NVIDIA GTX 550显卡,我听说它的性能并不出色。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接