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

时间:2010-04-12 20:00:06

标签: biginteger cpu-architecture processor

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

 246
 816
 * *
----
1062

其中*表示存在溢出。我在学校这样学习,所有BigInt添加函数我已经实现了类似于上面例子的工作。

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

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

所以我问自己:

  • 为什么我的处理器没有内置的BigInt功能?

它可以像任何其他BigInt库一样工作,只是(很多)更快,更低一级:处理器从缓存/ RAM中取一个数字,添加它,然后再将结果写回来。

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

7 个答案:

答案 0 :(得分:9)

有太多的问题要求处理器处理大量不合适的东西。

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

但是如果BigInt操作的结果超出了保留的空间量会怎么样?

有两种选择:

  1. 它会在它所拥有的空间内环绕 或
  2. 它会占用更多内存。
  3. 问题是,如果它确实1),那么它就没用了 - 你必须事先知道需要多少空间,这就是你想要使用BigInt的部分原因 - 所以你不是被那些东西所限制。

    如果它确实2),那么它必须以某种方式分配该内存。内存分配不是以相同的方式在操作系统上完成,但即使它是,它仍然必须更新所有指向旧值的指针。它如何知道什么是指向该值的指针,以及只是包含与所讨论的内存地址相同的值的整数值?

答案 1 :(得分:8)

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

答案 2 :(得分:3)

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

这会在处理器,操作系统内存管理和编译器之间产生大量的交互,而这些交互很难做到一般和高效。

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

答案 3 :(得分:3)

  

它可以像任何其他BigInt库一样工作,只是(很多)更快,更低一级:处理器从缓存/ RAM中取一个数字,添加它,然后再将结果写回来。

几乎所有 的CPU都具有此内置功能。您必须围绕相关指令使用软件循环,但如果循环有效,则不会使其变慢。 (由于部分标记停顿,在x86上非常重要,见下文)

e.g。如果x86提供rep adc来执行src + = dst,将2个指针和一个长度作为输入(如rep movsd到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不同,后者通常只有完全寄存器宽度。 (GMP calls each chunk a "limb")。 (x86有一些使用BCD或ASCII的指令,但这些指令已被删除x86-64。)

imul / idiv是签名的等价物。对于带符号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。 {1}}以及与adox有关,以及如何将它们串在一起以实现C源。

IDK _addcarryx内在函数的重点是什么,而不是仅仅让编译器使用adcx / adox来存在现有的_addcarry_u64内在函数,而是存在并行的dep链可以利用它。也许有些编译器对此并不够聪明。

以下是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之前,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上的多个uop,因此循环开销是一个问题,尤其是如果你有避免部分标志停顿的额外循环开销。如果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 uop(融合域),每1个时钟吞吐量一个,而adc m, r/i是4 uop(融合域),每2个时钟吞吐量一个。显然,Broadwell / Skylake将adc r,r/i作为单uop指令运行并没有帮助(利用具有3个输入依赖性的uops的能力,与Haswell for FMA一起引入)。我也不是100%肯定Agner的结果就在这里,因为他没有意识到SnB系列CPU只在解码器/ uop-cache中微熔融索引寻址模式,而不是无序核心。

无论如何,这个简单的未展开的循环是6 uops,并且应该在Intel SnB系列CPU上每2个循环运行一次。即使它需要额外的uop用于部分标记合并,这仍然很容易比8个可以在2个周期内发布的融合域uops小。

一些小的展开可以使每个循环接近1 adc,因为那部分只有4个uop。但是,每个周期2个负载和一个存储是不可持续的。

扩展精度乘法和除法也是可能的,利用加宽/缩小乘法和除法指令。当然,由于乘法的性质,它要复杂得多。

使用SSE进行附加携带或AFAIK任何其他BigInteger操作都没有用。

如果您正在设计新的指令集you can do BigInteger adds in vector registers if you have the right instructions to efficiently generate and propagate carry。该线程对硬件中支持进位标志的成本和优势进行了一些反复讨论,而软件生成的进位与MIPS相同:比较检测无符号环绕,将结果放入另一个整数寄存器。

答案 4 :(得分:1)

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

答案 5 :(得分:0)

有很多指令和功能在CPU芯片上争夺区域,最终那些被更频繁使用/被认为更有用的东西会推出那些不那么有用的东西。实现BigInt功能所需的指令就在那里,数学是直截了当的。

答案 6 :(得分:-1)

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

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

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

添加携带和减去更容易。等等

我刚在英特尔发布此消息: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx SSE4有一个BigInt LIbrary吗?

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来说并不是那么盛大,我听说。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx