为什么 Rust 的 u64.pow 需要 u32?

Why does Rust's u64.pow expect a u32?

为什么 Rust 的 u64 原语需要 u32 指数?

error[E0308]: mismatched types
  --> src/protagonists.rs:13:25
   |
13 |         return root.pow(self.secret) % prime;
   |                         ^^^^^^^^^^^ expected u32, found u64
help: you can convert an `u64` to `u32` and panic if the converted value wouldn't fit

https://doc.rust-lang.org/std/primitive.u64.html#pow.v

这是有根据的猜测。

考虑到 u64 提升到 u32 已经超过了溢出任何 u64 的能力。例如,如果我们取 2(最小可能有效的 u64)并将其提高到仅 2^10 或 1024,我们得到的结果比 u64 大得多。

179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216

允许更大的指数没有意义,它只是意味着更昂贵的计算更容易溢出。

如果 u64 不能安全地降级为 u32,它绝对不能安全地用作指数。 u128.pow 出于同样的原因也只采用 u32 指数。

注意:overflowing_pow 检测可能的溢出。

为什么大多数操作需要相同类型的操作数?

我们很容易看出2i32 + 2i64应该是4i64,但是对于CPU、2i322i64 是完全不同且完全不相关的事物。 CPU 中的 + 实际上只是一个硬件,通常支持两个 32 位输入或两个 64 位输入,但不支持一个 32 位输入和一个 64 位输入。因此,为了将 i32 添加到 i64,较短的数字必须是 sign-extended 到 64 位,然后才能将两个值插入 ALU。

大多数整数和 floating-point 算术运算通常也是如此:必须进行转换才能对不匹配的类型进行数学运算。在 C 中,编译器通常将两个操作数提升为可以表示两个值的最小类型;根据上下文,这些隐式转换称为 "integer promotions" or "usual arithmetic conversions"。然而,在 Rust 中,编译器大多只知道 same-type 操作,因此您必须通过决定如何转换操作数来选择您想要的操作类型。喜欢 Rust 的人通常认为这是一件好事。¹

为什么这不适用于 u64::pow

并非所有的算术运算,即使是那些在硬件中实现的,都接受相同类型的参数。在硬件中(虽然不是在 LLVM 中),移位指令通常会忽略移位参数的高位(这就是为什么在 C 中移位超过整数大小会调用未定义的行为)。 LLVM 提供 powi instructions 将 floating-point 数提高到整数次幂。

这些操作是不同的,因为输入是不对称的,设计人员经常利用这些不对称性来使硬件更快、更小。但是,在 u64::pow 的情况下,它 而不是 是用硬件指令实现的:it's just written in plain Rust. Bearing that in mind, it's clear that requiring the exponent to be a u64 is quite unnecessary: As 指出,u32 不仅能够包含所有u64 可以提高到任何精度的可能权力,所以额外的 32 位将毫无意义。

好的,为什么 u32

最后一句话对 u16 甚至 u8 同样成立 -- u64 不能包含 pow(2, 255),所以使用 u32 似乎几乎一样浪费。但是,也有实际的考虑。许多调用约定在寄存器中传递函数参数,因此在 32 位(或更大)平台上,您看不到比这更小的任何优势。许多 CPU 也不支持本机 8 位或 16 位算术,因此无论如何参数都必须是 sign-extended 才能实现我之前链接到的 exponentiation-by-squaring 算法。简而言之,我不知道为什么选择 u32,但像这样的事情可能已成为决定的因素。


¹ C 的规则在某种程度上受到历史的阻碍,并支持广泛的历史硬件。 Rust 仅针对 LLVM,因此编译器无需担心底层硬件是否具有原始 8 位 add 指令;它只是发出 add 并让 LLVM 担心它是被编译为原始指令还是被 32 位指令模拟。这就是为什么 char + char 在 C 中是 int,而 i8 + i8 在 Rust 中是 i8