为什么 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
这是有根据的猜测。
考虑到 u64 提升到 u32 已经超过了溢出任何 u64 的能力。例如,如果我们取 2(最小可能有效的 u64)并将其提高到仅 2^10 或 1024,我们得到的结果比 u64 大得多。
179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216
允许更大的指数没有意义,它只是意味着更昂贵的计算更容易溢出。
如果 u64 不能安全地降级为 u32,它绝对不能安全地用作指数。 u128.pow 出于同样的原因也只采用 u32 指数。
注意:overflowing_pow 检测可能的溢出。
为什么大多数操作需要相同类型的操作数?
我们很容易看出2i32 + 2i64
应该是4i64
,但是对于CPU、2i32
和2i64
是完全不同且完全不相关的事物。 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
。
为什么 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
这是有根据的猜测。
考虑到 u64 提升到 u32 已经超过了溢出任何 u64 的能力。例如,如果我们取 2(最小可能有效的 u64)并将其提高到仅 2^10 或 1024,我们得到的结果比 u64 大得多。
179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216
允许更大的指数没有意义,它只是意味着更昂贵的计算更容易溢出。
如果 u64 不能安全地降级为 u32,它绝对不能安全地用作指数。 u128.pow 出于同样的原因也只采用 u32 指数。
注意:overflowing_pow 检测可能的溢出。
为什么大多数操作需要相同类型的操作数?
我们很容易看出2i32 + 2i64
应该是4i64
,但是对于CPU、2i32
和2i64
是完全不同且完全不相关的事物。 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
。