Performance 为什么删除边界检查时代码运行较慢?
我正在用Rust写一个线性代数库 我有一个函数来获取对给定行和列的矩阵单元格的引用。此函数以一对断言开始,即行和列在边界内:Performance 为什么删除边界检查时代码运行较慢?,performance,optimization,rust,llvm-codegen,Performance,Optimization,Rust,Llvm Codegen,我正在用Rust写一个线性代数库 我有一个函数来获取对给定行和列的矩阵单元格的引用。此函数以一对断言开始,即行和列在边界内: #[inline(always)] pub fn get(&self, row: usize, col: usize) -> &T { assert!(col < self.num_cols.as_nat()); assert!(row < self.num_rows.as_nat()); unsafe {
#[inline(always)]
pub fn get(&self, row: usize, col: usize) -> &T {
assert!(col < self.num_cols.as_nat());
assert!(row < self.num_rows.as_nat());
unsafe {
self.get_unchecked(row, col)
}
}
奇怪的是,当我使用这些方法实现矩阵乘法(通过行和列迭代器)时,我的基准测试表明,当我检查边界时,它实际上要快33%。为什么会这样
我在两台不同的计算机上试过,一台运行Linux,另一台运行OSX,两台都显示了效果
完整的代码是。相关文件为。有关职能包括:
在第68行获取
取消选中第81行的
第551行next
第796行mul
- 第1038行的矩阵(基准)
最小性能
分支并运行货台
将对两种不同的乘法实现进行基准测试(注意,断言在该分支中首先被注释掉)
另外值得注意的是,如果我更改
get*
函数以返回数据的副本而不是引用(f64
而不是&f64
),效果就会消失(但代码的速度会稍微慢一些)。这不是一个完整的答案,因为我还没有测试我的声明,但这也许可以解释它。无论哪种方法,唯一确定的方法是生成LLVMIR和汇编程序输出。如果您需要LLVM IR的手册,可以在此处找到:
不管怎么说,这已经够了。假设您有以下代码:
#[inline(always)]
pub unsafe fn get_unchecked(&self, row: usize, col: usize) -> &T {
self.data.get_unchecked(self.row_col_index(row, col))
}
这里的编译器将其更改为间接加载,这可能会在紧循环中进行优化。值得注意的是,每个加载都有可能出错:如果您的数据不可用,它将触发越界
在结合了边界检查和紧循环的情况下,LLVM做了一些小动作。由于负载处于紧循环(矩阵乘法)中,并且边界检查的结果取决于循环的边界,因此它将从循环中删除边界检查并将其放在循环周围。换句话说,循环本身将保持完全相同,但需要额外的边界检查
换句话说,代码是完全相同的,只是有一些细微的差别
那么,发生了什么变化?两件事:
老问题又来了:您是否使用编译器优化(使用
--release
标志)编译过对锈蚀一无所知:你的基准测试是否正常?缓存效果、差异、测试数据同步…@LukasKalbertodt是的,我使用cargo bench
运行基准测试,它会随着发行版自动编译。也许编译器可以在检查边界时更积极地进行优化?对于类似的东西,最好在发行版模式下编译两个独立的二进制文件,并使用objdump-D
来识别相关紧循环中使用的机器指令。
#[inline(always)]
pub unsafe fn get_unchecked(&self, row: usize, col: usize) -> &T {
self.data.get_unchecked(self.row_col_index(row, col))
}