【问题标题】:Why doesn't my processor have built-in BigInt support?为什么我的处理器没有内置 BigInt 支持?
【发布时间】:2011-02-07 04:09:30
【问题描述】:

据我了解,BigInts 通常在大多数编程语言中实现为包含数字的数组,其中,例如:当添加其中两个时,每个数字都会像我们在学校所知道的那样一个接一个地添加,例如:

 246
 816
 * *
----
1062

其中 * 表示存在溢出。我在学校就是这样学的,我实现的所有 BigInt 添加功能都与上面的示例类似。

所以我们都知道我们的处理器只能本地管理从 0 到 2^32 / 2^64 的整数。

这意味着大多数脚本语言为了成为高级语言并提供具有大整数的算术运算,必须实现/使用将整数作为上述数组处理的 BigInt 库。 但这当然意味着它们会比处理器慢得多。

所以我问自己的是:

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

它可以像任何其他 BigInt 库一样工作,只是速度更快且级别更低:处理器从缓存/RAM 中获取一个数字,添加它,然后再次写回结果。

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

【问题讨论】:

  • BigInts 不是用字符串实现的,它们是用字节数组实现的。但是,如果您将字节数组视为 base-256 表示法的字符串,那么您所说的是正确的。
  • 为什么处理器中没有动态小马和独角兽绘制例程!
  • 当 CPU 寄存器只有 8 位宽时,对 bigint 数学的软件库的兴趣更为普遍。当时进行重要数学运算的唯一方法是通过软件库。现在几乎到处都有 64 位整数寄存器和硬件浮点,使用软件库进行简单的数学运算更像是一种好奇心,而不是关键需求。
  • 一旦您进行较大的非固定大小的运算(尤其是乘法/除法),就有许多可能的实现选择,每种选择都有自己的权衡(并且差异很大)。仅将其硬编码到处理器中就像使用某个版本的 GMP,而没有升级或更改选项。此外,GMP 之类的库非常庞大,而芯片制造商则希望其操作相对简单且可验证。
  • @dthorpe 英特尔® 架构处理器上引入了新指令,以实现大整数运算的快速实施。

标签: biginteger cpu-architecture processor


【解决方案1】:

有太多的问题需要处理器处理大量不是它的工作的东西。

假设处理器 DID 具有该功能。我们可以制定一个系统,让我们知道给定 BigInt 使用了多少字节 - 只需使用与大多数字符串库相同的原理并记录长度即可。

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

有两种选择:

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

问题是,如果它是 1),那么它就没有用 - 你必须事先知道需要多少空间,这也是你想要使用 BigInt 的部分原因 - 所以你不是受这些东西的限制。

如果它执行了 2),那么它将不得不以某种方式分配该内存。跨操作系统的内存分配方式不同,但即使是这样,它仍然必须更新所有指向旧值的指针。它怎么知道什么是指向该值的指针,什么是简单的整数值,包含与所讨论的内存地址相同的值?

【讨论】:

  • 处理器 do 支持带进位的加法运算,并乘以产生完整的低和高结果 (64b * 64b => 128b)。软件必须在循环中使用这些,但这并不比微码循环慢(比如如果rep adcq 存在执行 src += dst,将 2 个指针和一个长度作为输入)。我添加了一个包含更多细节的答案。
【解决方案2】:

它可以像任何其他 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,adcdec 写入其他标志后读取 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 tablesadc 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 那样生成进位:比较以检测无符号环绕,将结果放入另一个整数寄存器中。

【讨论】:

【解决方案3】:

似乎英特尔正在添加(或已添加为 @time of this post - 2015)对大整数运算的新指令支持。

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

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

【讨论】:

  • 这是在 Broadwell 中添加的 ADX 指令集扩展 (mulx / adcx / adox)(由 CPUID 中的 ADX 功能位指示)。常规的adcmul 已经存在;如果您可以同时保持两个带有进位的加法指令流,并且乘法交错,那么这些新指令只会增加并行性。这些指令只使用 EFLAGS 中的一个位进行进位/出位,不会影响其他指令。
【解决方案4】:

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

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

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

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

我刚刚在英特尔发布了这个: 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,我听说它在未签名的 Ints 上并不那么出色。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

【讨论】:

    【解决方案5】:

    有太多的指令和功能在 CPU 芯片上争夺区域,最终那些使用频率更高/被认为更有用的指令和功能最终会淘汰那些不常用的指令和功能。那里有实现 BigInt 功能所需的说明,而且数学很简单。

    【讨论】:

      【解决方案6】:

      假设乘法的结果需要 3 倍的空间(内存)来存储 - 处理器会将结果存储在哪里?该结果的用户(包括指向它的所有指针)如何知道它的大小突然改变了 - 并且改变大小可能需要它在内存中重新定位它,因为扩展当前位置会与另一个变量发生冲突。

      这会在处理器、操作系统内存管理和编译器之间产生大量交互,这将很难实现通用和高效。

      管理应用程序类型的内存不是处理器应该做的事情。

      【讨论】:

      • IIRC,乘法结果只需要等于操作数宽度总和的位数。因此,假设我们将两个大小相等的变量相乘,则结果所需的空间永远不会超过 2 倍。
      • 当然,但是直到运行时你才知道这个大小,这是一个难题。
      • @leeeroy 在运行时之前您几乎不知道任何东西的大小。它的难点是什么?只需计算字节数并分配足够的内存来保存此类操作的结果。因为如果你说的话有任何意义,我们不应该使用计算机,因为在某些时候它们可能无法保存我们的数据。现在有很多 GB 的内存,BCD 几乎不会达到极限。
      【解决方案7】:

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

      【讨论】:

      • 我认为处理 x86/x87/MMX/SSE/SSE2/SSE3/SSSE3 的处理器的设计不一定以最小化指令集为目标。
      【解决方案8】:

      Binary Coded Decimal 是一种字符串数学形式。 Intel x86 处理器具有direct BCD arthmetic operations 的操作码。

      【讨论】:

      • 几乎没有人使用。
      • @mmyers - 今年不是因为 Windows 团队中的某个人将 BCD 转换为十进制而导致大量计算机将年份显示为 2016 年吗?
      • @mmyers:OP 没有问它是否受欢迎。 :P BCD 在金融应用中用于定点数学,以避免丢失精度/舍入误差。
      • +1 表示提及。此外,有趣的是(或不是),IEEE-754 定义了二进制和十进制基数变体。 en.wikipedia.org/wiki/IEEE_754-2008.
      猜你喜欢
      • 2012-05-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多