【问题标题】:Is it possible to get the native CPU size of an integer in Rust?是否有可能在 Rust 中获得整数的本机 CPU 大小?
【发布时间】:2020-09-10 10:08:16
【问题描述】:

为了好玩,我正在用 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 大小与指针宽度不同。因此,我的解决方案不适用于所有架构。另一种可能的解决方案是编写一个构建脚本,该脚本生成一个小模块,该模块根据usize 的大小定义LargeInt(我们可以像这样获取:std::mem::size_of::<usize>()。)但是,这与上面,因为usize is based on the pointer width 也是如此。最后一个明显的解决方案是简单地为每个架构保留一个原生 int 大小的映射。但是,此解决方案不优雅且无法很好地扩展,因此我想避免使用它。

所以,我的问题是:有没有办法找到目标的本机 int 大小,最好是在编译之前,以减少运行时开销?这种努力是否值得?也就是说,使用本机 int 大小与使用指针宽度之间是否可能存在显着差异?

【问题讨论】:

    标签: optimization rust cpu-architecture bigint


    【解决方案1】:

    通常很难(或不可能)让编译器为 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_nmpn_sub_n(添加 OR Sub = aors),针对没有部分标志停顿的 SandyBridge-family 进行了调整,因此它可以与 dec/jnz 循环。

    在使用高级语言编写代码时,了解哪种 asm 是最佳的可能会有所帮助。虽然在实践中你甚至无法接近它,所以有时使用不同的技术是有意义的,比如在 32 位整数中只使用高达 2^30 的值(就像 CPython 在内部所做的那样,通过右移,请参阅the section about Python in this)。在 Rust 中,您确实可以访问 add_overflow 来获得结转,但使用它仍然很困难。

    对于实际使用,为 GMP 编写 Rust 绑定可能是最好的选择,除非已经存在。

    尽可能使用最大的块是非常好的;在所有当前的 CPU 上,add reg64, reg64 具有与add reg32, reg32reg8 相同的吞吐量和延迟。因此,您每单位完成的工作量是原来的两倍。并在 1 个延迟周期内通过 64 位结果进行传播。

    (有其他存储 BigInteger 数据的方法可以使 SIMD 有用;@Mysticial 在Can long integer routines benefit from SSE? 中解释。例如,每个 32 位 int 有 30 个值位,允许您将规范化推迟到几个加法步骤之后。但是每个使用这些数字必须意识到这些问题,所以它不是一个简单的替代品。)


    在 Rust 中,您可能只想使用 u64 而不管目标是什么,除非您真的关心 32 位目标上的小数(单肢)性能。让编译器从add/adc(带进位相加)中为您构建 u64 操作。

    唯一可能需要特定于 ISA 的情况是 u128 在某些目标上不可用。您想使用 64 * 64 => 128 位全乘法作为乘法的构建块;如果编译器可以使用u128 为您做到这一点,那就太好了,特别是如果它有效地内联。

    另见问题下 cmets 中的讨论。

    让编译器发出有效的 BigInt 加法循环(即使在一个展开循环的主体内)的一个绊脚石是编写一个加法,它接受一个进位输入并产生一个进位输出。请注意,x += 0xff..ff + carry=1 需要产生执行,即使 0xff..ff + 1 回绕为零。所以在 C 或 Rust 中,x += y + carry 必须检查 y+carryx+= 部分中的执行。

    要说服像 LLVM 这样的编译器后端发出 adc 指令链真的很难(可能是不可能的)。当您不需要从 adc 执行时,可以使用 add/adc。或者,如果编译器正在为你做 u128.overflowing_add

    通常编译器会将进位标志转换为寄存器中的 0 / 1,而不是使用 adc。此外,通过将输入 u64 值与 u128.overflowing_add 的 u128 组合起来,您至少可以避免成对的 u64 出现这种情况。希望这不会花费任何 asm 指令,因为 u128 已经必须存储在两个单独的 64 位寄存器中,就像两个单独的 u64 值一样。

    因此,最多组合 u128 可能只是对添加 u64 元素数组的函数的局部优化,以减少编译器的负担。

    【讨论】:

      【解决方案2】:

      在我的图书馆ibig 我所做的是:

      1. 根据target_arch 选择特定于架构的大小。
      2. 如果我没有架构的值,请根据 target_pointer_width 选择 16、32 或 64。
      3. 如果 target_pointer_width 不是这些值之一,请使用 64。

      【讨论】:

      • 因此,在 64 位机器上的 ILP32 ABI(如 Linux x32 或 AArch64 ILP32)上,您最终会选择 32。避免这是问题的重点。但鉴于没有简单的方法可以做得更好,是的,很多人都在做你正在做的事情。
      • @PeterCordes 这是不正确的。我刚刚尝试使用 x32 目标(x86_64-unknown-linux-gnux32),它选择了 64 位。 “target_arch=x86_64”,因此它在步骤 1 中选择了 64 位。
      • 哦,我明白了,我误解了您的意思,您对一些已知架构进行了特殊处理。
      • 感谢您的评论 - 它帮助我意识到我需要在我的列表中添加对其他架构的支持,例如 AArch64,即使我没有专门的代码,只是为了选择最佳位大小。
      猜你喜欢
      • 2011-08-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-06
      • 2010-11-08
      • 1970-01-01
      • 1970-01-01
      • 2020-12-22
      相关资源
      最近更新 更多