在 Rust 中,默认情况下当出现整型溢出时,Debug 模式会发生 panic,Release 模式下会在溢出后取舍归零;
同时,Rust 提供了显式的方法来控制行为;
源代码:
在Rust中处理整数溢出
默认行为
默认情况下,当出现整型溢出时,Debug 模式会发生 panic,Release 模式下会在溢出后取舍归零;
src/main.rs
1 |
|
Rust 的编译器是非常智能的,会在编译期就检测出上面的代码存在溢出;
此时可以通过添加:#[allow(arithmetic_overflow)] 来关闭该检查;
debug模式:
1 | cargo run |
release模式:
1 | cargo run --release |
该差异的来源在于:dev 和 release 配置的定义不同(参见:Profiles - The Cargo Book);
造成这一行为的选项是 overflow-checks,它在 dev 中开启,release 中关闭;
如果你的应用程序依赖于整数溢出行为,可以直接更改 dev 配置,以关闭 overflow-checks:
1 | [profile.dev] |
显式运算
对于所有的有符号和无符号整数,Rust 提供了四组不同的运算函数,这提供了显式处理整数溢出的方式;
wrapping_ 系列函数:
examples/1_opt.rs
1 | fn wrapping_demo() { |
wrapping_ 系列函数处理整数溢出的方法是回绕,即从整数类型的最大值回绕到最小值(也是我们期望发生的默认情况);
这种方法确保了在使用这些函数时,无论构建配置文件如何,都不会造成意外的运行时错误;
overflowing_系列函数:
examples/1_opt.rs
1 | fn overflowing_demo() { |
这些函数等同于 wrapping_,除了返回值会多一个 bool 以指明是否有溢出产生;
例如:在实现模拟器时可能特别有用,因为许多 CPU 有一个标志,且必须在指令导致溢出时设置;
checked_系列函数:
examples/1_opt.rs
有时我们不想回绕值,而是将溢出作为一种特殊情况处理。可以通过 checked_ 达到这一效果:
1 | fn checked_demo() { |
saturating_系列函数:
examples/1_opt.rs
另一种选择是在溢出时饱合(saturating),而非回绕(即到达最大值或最小时,保持该值):
1 | fn saturating_demo() { |
执行额外开销
你可能会担心,每当想执行基本的运算时,多余的函数调用会减慢代码的执行速度;实际上 Rust 可以优化掉多余的函数调用;
我们可以通过使用 cargo-show-asm 来查看某个函数编译后的汇编指令;
先看看普通的加法,和编译后的汇编:
1 | pub fn addition(x: u8, y: u8) -> u8 { |
编译的结果是单条 lea 指令。再来看看 wrapping_add:
1 | pub fn addition(x: u8, y: u8) -> u8 { |
正如我们期望的,编译的结果相同:wrapping_add 的调用已经被优化!
包装类型
在某些场景中,有许多地方都可能发生整数溢出,那么上述方法就会显得有些冗长,很多时候还容易忘记处理整数溢出;
Rust 也提供了 Wrapping<T> 包装类型,这种类型允许使用正常的算术操作符,同时确保在整数溢出时自动回绕!
例如:
examples/2_warpping.rs
1 | use std::num::Wrapping; |
这比在每个运算时都使用 wrapping_ 函数显得更清晰!
也有一个类似的 Saturating<T>,和 Wrapping<T> 类似,但在溢出时饱合而非回绕;
Saturating<T>已于 2023 年 11 月发布的 Rust 1.74.0 中稳定:#115477
附录
源代码:
参考文章: