在 Rust 中链接检查算术运算
Chaining checked arithmetic operations in Rust
在进行带有溢出检查的整数运算时,计算往往需要组合多个算术运算。在 Rust 中链接检查算法的一种直接方法使用 checked_*
方法和 Option
链接:
fn calculate_size(elem_size: usize,
length: usize,
offset: usize)
-> Option<usize> {
elem_size.checked_mul(length)
.and_then(|acc| acc.checked_add(offset))
}
但是,这会告诉编译器为每个基本操作生成一个分支。我遇到了一种使用 overflowing_*
方法的更展开的方法:
fn calculate_size(elem_size: usize,
length: usize,
offset: usize)
-> Option<usize> {
let (acc, oflo1) = elem_size.overflowing_mul(length);
let (acc, oflo2) = acc.overflowing_add(offset);
if oflo1 | oflo2 {
None
} else {
Some(acc)
}
}
无论溢出如何继续计算并使用按位或聚合溢出标志确保在整个评估中最多执行一个分支(假设 overflowing_*
的实现生成无分支代码)。这种优化友好的方法比较麻烦,在处理中间值时需要谨慎。
有没有人知道 Rust 编译器如何在各种 CPU 架构上优化上述任一模式,以判断显式展开是否值得,尤其是对于更复杂的表达式?
Does anyone have experience with how the Rust compiler optimizes either of the patterns above on various CPU architectures, to tell whether the explicit unrolling is worthwhile, especially for more complex expressions?
您可以使用 playground to check how LLVM optimizes things: just click on "LLVM IR" or "ASM" instead of "Run". Stick a #[inline(never)]
on the function you wish to check, and pay attention to pass it run-time arguments, to avoid constant folding. As in here:
use std::env;
#[inline(never)]
fn calculate_size(elem_size: usize,
length: usize,
offset: usize)
-> Option<usize> {
let (acc, oflo1) = elem_size.overflowing_mul(length);
let (acc, oflo2) = acc.overflowing_add(offset);
if oflo1 | oflo2 {
None
} else {
Some(acc)
}
}
fn main() {
let vec: Vec<usize> = env::args().map(|s| s.parse().unwrap()).collect();
let result = calculate_size(vec[0], vec[1], vec[2]);
println!("{:?}",result);
}
然而,你会得到的答案是,不幸的是,Rust 和 LLVM 中的溢出内在函数是为了方便而不是为了性能而编码的。这意味着虽然显式展开优化得很好,但现在指望 LLVM 来优化检查代码是不现实的。
通常这不是问题;但对于性能热点,您可能需要手动展开。
注意:这种性能不足也是 Release 模式下默认禁用溢出检查的原因。
在进行带有溢出检查的整数运算时,计算往往需要组合多个算术运算。在 Rust 中链接检查算法的一种直接方法使用 checked_*
方法和 Option
链接:
fn calculate_size(elem_size: usize,
length: usize,
offset: usize)
-> Option<usize> {
elem_size.checked_mul(length)
.and_then(|acc| acc.checked_add(offset))
}
但是,这会告诉编译器为每个基本操作生成一个分支。我遇到了一种使用 overflowing_*
方法的更展开的方法:
fn calculate_size(elem_size: usize,
length: usize,
offset: usize)
-> Option<usize> {
let (acc, oflo1) = elem_size.overflowing_mul(length);
let (acc, oflo2) = acc.overflowing_add(offset);
if oflo1 | oflo2 {
None
} else {
Some(acc)
}
}
无论溢出如何继续计算并使用按位或聚合溢出标志确保在整个评估中最多执行一个分支(假设 overflowing_*
的实现生成无分支代码)。这种优化友好的方法比较麻烦,在处理中间值时需要谨慎。
有没有人知道 Rust 编译器如何在各种 CPU 架构上优化上述任一模式,以判断显式展开是否值得,尤其是对于更复杂的表达式?
Does anyone have experience with how the Rust compiler optimizes either of the patterns above on various CPU architectures, to tell whether the explicit unrolling is worthwhile, especially for more complex expressions?
您可以使用 playground to check how LLVM optimizes things: just click on "LLVM IR" or "ASM" instead of "Run". Stick a #[inline(never)]
on the function you wish to check, and pay attention to pass it run-time arguments, to avoid constant folding. As in here:
use std::env;
#[inline(never)]
fn calculate_size(elem_size: usize,
length: usize,
offset: usize)
-> Option<usize> {
let (acc, oflo1) = elem_size.overflowing_mul(length);
let (acc, oflo2) = acc.overflowing_add(offset);
if oflo1 | oflo2 {
None
} else {
Some(acc)
}
}
fn main() {
let vec: Vec<usize> = env::args().map(|s| s.parse().unwrap()).collect();
let result = calculate_size(vec[0], vec[1], vec[2]);
println!("{:?}",result);
}
然而,你会得到的答案是,不幸的是,Rust 和 LLVM 中的溢出内在函数是为了方便而不是为了性能而编码的。这意味着虽然显式展开优化得很好,但现在指望 LLVM 来优化检查代码是不现实的。
通常这不是问题;但对于性能热点,您可能需要手动展开。
注意:这种性能不足也是 Release 模式下默认禁用溢出检查的原因。