据我了解,BigInts通常在大多数编程语言中实现为包含数字的数组,例如:当添加其中两个时,每个数字都是一个接一个地添加,就像我们从学校知道的那样,例如:
246
816
* *
----
1062
其中*表示存在溢出。我在学校这样学习,所有BigInt添加函数我已经实现了类似于上面例子的工作。
所以我们都知道我们的处理器只能本地管理从0到2^32
/ 2^64
的整数。
这意味着大多数脚本语言为了高级并提供具有大整数的算术,必须实现/使用BigInt库,这些库使用整数作为上面的数组。 但当然这意味着它们将比处理器慢得多。
所以我问自己:
它可以像任何其他BigInt库一样工作,只是(很多)更快,更低一级:处理器从缓存/ RAM中取一个数字,添加它,然后再将结果写回来。
对我来说似乎是一个好主意,为什么没有这样的东西呢?
答案 0 :(得分:9)
有太多的问题要求处理器处理大量不合适的东西。
假设处理器DID具有该功能。我们可以设计一个系统,我们知道给定BigInt使用了多少字节 - 只需使用与大多数字符串库相同的原理并记录长度。
但是如果BigInt操作的结果超出了保留的空间量会怎么样?
有两种选择:
问题是,如果它确实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很有用,可以实现具有单周期延迟的真正宽加法器。
adc
: add with carry / sbb
: subtract with borrow mul
: full multiply, producing upper and lower halves of the result:例如64b * 64b => 128B div
: dividend is twice as wide as the other operands,例如128b / 64b => 64b师。当然,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中没有内在函数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链可以利用它。也许有些编译器对此并不够聪明。
;;;;;;;;;;;; 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之前,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上的多个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 tables,adc 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