6 个版本 (破坏性更新)
使用旧的 Rust 2015
0.5.0 | 2023 年 3 月 25 日 |
---|---|
0.4.0 | 2021 年 10 月 15 日 |
0.3.0 | 2021 年 10 月 2 日 |
0.2.1 | 2021 年 9 月 21 日 |
0.1.0 | 2021 年 9 月 19 日 |
#935 in Rust 模式
每月 55 次下载
在 2 个crate中使用(通过 bitint)
20KB
130 行
假设
在 Rust 中声明不安全假设的宏。
使用此宏,可以为编译器提供用于优化的假设。这些假设在 debug_assertion
配置中进行检查,在其他情况下则不进行检查(但仍然存在)。
这是一个固有的不安全操作。它存在于常规 assert!
和纯 unsafe
访问之间的空间 - 它严重依赖于优化编译器跟踪不可达路径以消除不必要的断言的能力。
[dependencies]
assume = "0.5"
示例
use assume::assume;
let v = vec![1, 2, 3];
// Some computed index that, per invariants, is always in bounds.
let i = get_index();
assume!(
unsafe: i < v.len(),
"index {} is beyond vec length {}",
i,
v.len(),
);
let element = v[i]; // Bounds check optimized out per assumption.
use assume::assume;
let items: HashMap<u32, String> = populate_items();
// Some item that, per invariants, always exists.
let item_zero_opt: Option<&String> = items.get(&0);
assume!(
unsafe: item_zero_opt.is_some(),
"item zero missing from items map",
);
let item_zero = item_zero_opt.unwrap(); // Panic check optimized out per assumption.
use assume::assume;
enum Choices {
This,
That,
Other,
}
// Some choice that, per invariants, is never Other.
let choice = get_choice();
match choice {
Choices::This => { /* ... */ },
Choices::That => { /* ... */ },
Choices::Other => {
// This case optimized out entirely, no panic emitted.
assume!(
unsafe: @unreachable,
"choice was other",
);
},
}
use assume::assume;
#[inline(always)]
fn compute_value() -> usize {
let result = compute_value_internal();
// Can also be used to provide hints to the caller,
// after the optimizer inlines this assumption.
assume!(
unsafe: result < 12,
"result is invalid: {}",
result,
);
result
}
fn compute_value_internal() -> usize {
/* ... */
}
fn process_data(data: &[f64; 100]) {
// Bounds check elided per implementation's assumption.
let value = data[compute_value()];
}
动机
程序往往有类型系统无法表达的不变量。Rust 默认是安全的,在这种情况下,运行时断言被用来验证这些不变量。这种情况的常见示例是切片的边界检查。
考虑以下(有些复杂)的示例
pub struct ValuesWithEvens {
values: Vec<u32>, // Some integers.
evens: Vec<usize>, // Indices of even integers in `values`.
}
impl ValuesWithEvens {
pub fn new(values: Vec<u32>) -> Self {
let evens = values
.iter()
.enumerate()
.filter_map(
|(index, value)| {
if value % 2 == 0 {
Some(index)
} else {
None
}
}
)
.collect();
Self { values, evens }
}
pub fn pop_even(&mut self) -> Option<u32> {
let index = self.evens.pop()?;
// We know this index is valid by construction,
// but a bounds check is performed anyway.
let value = self.values[index];
Some(value)
}
}
fn main() {
let mut vwe = ValuesWithEvens::new(vec![1, 2, 3, 4]);
println!("{:?}", vwe.pop_even());
}
按照构造,evens
中包含的索引始终是 values
的有效索引。然而,这不能在类型系统中表达,并且在第 values
行有一个边界检查。
let value = self.values[index];
这确保了程序中的错误不会导致越界访问。例如,如果引入了另一个方法来修改 values
但忘记了更新 evens
,它将使索引无效 - 这不会因为边界检查而导致未定义的行为。
然而,如果这是程序的热点,我们可能需要删除这个检查。Rust 提供了 unsafe
访问
pub fn pop_even(&mut self) -> Option<u32> {
let index = self.evens.pop()?;
let value = unsafe { *self.values.get_unchecked(index) };
Some(value)
}
这没有边界检查,但我们已经消除了对访问的任何审查。我们可以使用仅调试断言来改进这一点
pub fn pop_even(&mut self) -> Option<u32> {
let index = self.evens.pop()?;
debug_assert!(index < self.evens.len());
let value = unsafe { *self.values.get_unchecked(index) };
Some(value)
}
你能找到错误吗?我们针对错误的向量进行了断言!这应该是
debug_assert!(index < self.values.len());
// ^^^^^^
将断言与优化分离是容易出错的。
assume!
宏依赖于优化器利用已声明假设的能力。错误的假设会保留边界检查,但正确的假设会移除它。
pub fn pop_even(&mut self) -> Option<u32> {
let index = self.evens.pop()?;
assume!(
unsafe: index < self.values.len(),
"even index {} beyond values vec length {}",
index,
self.values.len(),
);
let value = self.values[index];
Some(value)
}
优化器根据假设将边界检查视为死代码,因此将其移除。此外,这将在 debug_assertion
配置中(例如在测试中)断言我们的条件成立。
实现也可以通过调用者上下文提供假设。
#[inline(always)]
pub fn pop_even(&mut self) -> Option<u32> {
let value = self.pop_even_internal()?;
assume!(
unsafe: value % 2 == 0,
"popped value {} is not even",
value,
);
value
}
fn pop_even_internal(&mut self) -> Option<u32> {
/* ... */
}
调用者现在可以“免费”获得优化。例如
fn compute_something(vwe: &mut ValuesWithEvens) -> Option<f64> {
let value = vwe.pop_even()?;
perform_common_task(value)
}
fn perform_common_task(value: u32) -> Option<f64> {
if value % 2 == 0 {
/* ... */
} else {
// This branch is now considered dead code when the
// function is called from the `compute_something` path.
}
}
何时不使用
不要使用此宏。
依靠 assert!
检查程序的不变量。
依靠 unreachable!
表明某些代码路径永远不应该被采取。
何时使用
好吧 - 一旦你
- 有分析结果表明某些不变量检查导致了开销。
- 没有方法重新安排程序以无开销地表达这一点。
- 你即将进行一个
unsafe
获取操作。
那么你应该考虑使用 assume!
。
这不是一个面向初学者的宏;你必须验证所需的优化正在发生。你还应该有一个测试套件,这些测试在 debug_assertion
启用的情况下构建,以便捕获不变量的违规行为。
注意事项
-
与
debug_assert!
等类似,assume!
的条件始终存在 - 被移除的是恐慌。涉及函数调用和副作用复杂假设不太可能是有帮助的;条件应该是微不足道的,并且仅涉及立即可用的事实。 -
正如所述,这依赖于优化器传播假设。优化级别或编译器情绪的差异可能导致它在最终输出中无法省略断言。如果您 必须 没有检查并且不希望依赖优化,则
debug_assert!
+unsafe
访问是最佳选择。 -
避免使用
assume!(unsafe: false)
来指示不可达的代码。虽然这有效,但返回类型是()
而不是!
。这可能会导致例如其他分支评估为除()
之外的类型时的警告或错误。请使用assume!(unsafe: @unreachable)
代替。
另请参阅
该宏的底层机制是 std::hint::unreachable_unchecked
。
许可
根据您的选择,许可协议为
- Apache License,版本 2.0(LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT 许可证(LICENSE-MIT 或 http://opensource.org/licenses/MIT)
。
贡献
除非您明确声明,否则根据Apache-2.0许可证定义,您提交的任何有意包含在作品中的贡献,将以上述方式双许可,不附加任何额外条款或条件。