Performance 为什么删除边界检查时代码运行较慢?

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 {

我正在用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 {
        self.get_unchecked(row, col)
    }
}
奇怪的是,当我使用这些方法实现矩阵乘法(通过行和列迭代器)时,我的基准测试表明,当我检查边界时,它实际上要快33%。为什么会这样

我在两台不同的计算机上试过,一台运行Linux,另一台运行OSX,两台都显示了效果

完整的代码是。相关文件为。有关职能包括:

  • 在第68行获取
  • 取消选中第81行的
  • next
    第551行
  • mul
    第796行
  • 第1038行的矩阵(基准)
请注意,我使用类型级别的数字来参数化我的矩阵(通过虚拟标记的类型也可以选择动态大小),因此基准是将两个100x100矩阵相乘

更新:

我已经大大简化了代码,删除了在基准测试中没有直接使用的东西,并删除了通用参数。我还编写了一个不使用迭代器的乘法实现,该版本不会产生相同的效果。有关此版本的代码,请参阅。克隆
最小性能
分支并运行
货台
将对两种不同的乘法实现进行基准测试(注意,断言在该分支中首先被注释掉)


另外值得注意的是,如果我更改
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做了一些小动作。由于负载处于紧循环(矩阵乘法)中,并且边界检查的结果取决于循环的边界,因此它将从循环中删除边界检查并将其放在循环周围。换句话说,循环本身将保持完全相同,但需要额外的边界检查

换句话说,代码是完全相同的,只是有一些细微的差别

那么,发生了什么变化?两件事:

  • 如果我们有额外的边界检查,就不可能再出现越界负载。这可能会触发以前不可能的优化。不过,考虑到这些检查通常是如何实现的,这不是我的猜测

  • 另一个要考虑的是,“不安全”这个词可能会触发一些行为,比如附加条件、PIN数据或禁用GC等。了解这些细节的唯一方法是查看LLVM IR


    老问题又来了:您是否使用编译器优化(使用
    --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))
    }