为了好玩,我正在Rust中编写一个bignum库。我的目标(与大多数bignum库一样)是使它尽可能高效。我希望即使在不寻常的体系结构上,它也能保持高效。
在我看来,CPU可以更快地对具有体系结构的本机位数的整数执行算术运算(即u64
对于64位计算机,u16
对于16位计算机因此,由于我想创建一个在所有架构上都有效的库,因此我需要考虑目标架构的本机整数大小。显而易见的方法是使用cfg attribute target_pointer_width 。例如,定义最小的类型,该类型将始终能够容纳比最大本地int大小更大的值:
#[cfg(target_pointer_width = "16")]
type LargeInt = u32;
#[cfg(target_pointer_width = "32")]
type LargeInt = u64;
#[cfg(target_pointer_width = "64")]
type LargeInt = u128;
但是,在研究这个问题时,我遇到了this comment。它给出了一个体系结构的示例,其中本机int大小与指针宽度不同。因此,我的解决方案不适用于所有架构。另一个可能的解决方案是编写一个生成脚本,该脚本对一个小的模块进行代码生成,该模块根据LargeInt
的大小定义usize
(我们可以像这样获得std::mem::size_of::<usize>()
。)但是,与usize
is based on the pointer width一样,这也具有与上述相同的问题。最后一个显而易见的解决方案是为每个体系结构简单地保留一个本地int大小的映射。但是,此解决方案不够完善,无法很好地扩展,因此我想避免使用它。
所以,我的问题是:有一种方法可以找到目标的本机int大小,最好是在编译之前,以减少运行时开销?这样做值得吗?也就是说,使用本地int大小与指针宽度之间是否可能存在显着差异?
答案 0 :(得分:7)
通常很难(或不可能)使编译器为BigNum生成最佳代码,这就是https://gmplib.org/拥有手写的低级基本函数(mpn_...
docs)的原因各种目标体系结构的装配,并针对不同的 micro 体系结构进行调整,例如https://gmplib.org/repo/gmp/file/tip/mpn/x86_64/core2/mul_basecase.asm用于多边*多边数字的一般情况。 https://gmplib.org/repo/gmp/file/tip/mpn/x86_64/coreisbr/aors_n.asm用于mpn_add_n
和mpn_sub_n
(添加OR子= aors),针对没有部分标志停顿的SandyBridge系列进行了调整,因此可以与dec/jnz
一起循环
用高级语言编写代码时,了解哪种asm最佳可能会有所帮助。尽管实际上您甚至无法做到这一点,所以有时使用另一种技术还是很有意义的,例如仅使用32位整数中最多2 ^ 30的值(例如CPython在内部进行,通过右移,请参阅the section about Python in this)。在Rust中,您确实可以访问add_overflow
来进行结转,但是仍然很难使用。
对于实际用途,最好为GMP编写Rust绑定,除非已经存在。
使用尽可能大的块非常好;在所有当前CPU上,add reg64, reg64
具有与add reg32, reg32
或reg8
相同的吞吐量和延迟。这样您就可以完成每单位两倍的工作量。并通过64位结果进行传播,从而延迟了1个周期。
(还有其他一些方法可以存储可以使SIMD有用的BigInteger数据; @Mysticial在Can long integer routines benefit from SSE?中进行了说明。例如,每32位int 30个值位,使您可以将归一化推迟到几个附加步骤之后。但是,每次使用此类数字都必须意识到这些问题,因此这不是一个简单的替代方法。)
在Rust中,您可能只想使用u64
而不考虑目标,除非您真的关心32位目标的少量(单边缘)性能。让编译器从add
/ adc
(带有进位添加)中为您构建u64操作。
唯一需要做的事情是特定于ISA的情况是:u128
在某些目标上不可用。您想使用64 * 64 => 128位全乘法作为乘法的构建块;如果编译器可以使用u128
为您做到这一点,那很好,特别是如果它有效地内联。
另请参阅问题注释中的讨论。
一个使编译器发出有效的BigInt加法循环(甚至在一个展开的循环内)的绊脚石是编写一个带有进位输入并产生进位输出的加法。请注意,即使x += 0xff..ff + carry=1
换为零,也需要产生一个进位。因此,在C或Rust中,0xff..ff + 1
必须检查x += y + carry
和y+carry
部分中的执行情况。
要说服LLVM之类的编译器后端发出一系列ADC指令真的很难(可能是不可能的)。当您不需要从ADC结转时,可以执行add / adc。或者,如果编译器正在为您x+=
通常,编译器会将进位标志变成寄存器中的0/1,而不是使用u128.overflowing_add
。您可以通过将输入的u64值组合到adc
的u128中,从而希望至少对u64
成对使用。希望这不会花费任何asm指令,因为u128.overflowing_add
已经必须存储在两个单独的64位寄存器中,就像两个单独的u128
值一样。
因此,最多合并u64
可能只是对函数的局部优化,该函数添加了u128
元素的数组,以使编译器的工作量减少。
答案 1 :(得分:1)
在我的图书馆 ibig 我所做的是:
target_arch
选择特定于架构的大小。target_pointer_width
选择 16、32 或 64。target_pointer_width
不是这些值之一,请使用 64。