当我删除边界检查时,为什么我的代码运行速度较慢?

时间:2016-05-29 20:57:04

标签: performance optimization rust

我正在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)
    }
}

在紧密循环中,我认为跳过边界检查可能会更快,所以我提供了get_unchecked方法:

#[inline(always)]
pub unsafe fn get_unchecked(&self, row: usize, col: usize) -> &T {
    self.data.get_unchecked(self.row_col_index(row, col))
}

奇怪的是,当我使用这些方法实现矩阵乘法(通过行和列迭代器)时,我的基准测试表明,当我检查边界时,它实际上要快33%。为什么会这样?

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

完整代码为on github。相关文件为lib.rs。感兴趣的功能是:

  • get第68行
  • get_unchecked第81行
  • next第551行
  • mul第796行
  • matrix_mul(基准)第1038行

请注意,我正在使用类型级数来对我的矩阵进行参数化(通过虚拟标记类型也可以选择动态大小),因此基准测试将两个100x100矩阵相乘。

更新

我已经大大简化了代码,删除了未直接在基准测试中使用的内容并删除了泛型参数。我还编写了一个不使用迭代器的乘法实现,而版本不会产生相同的效果。有关此版本的代码,请参阅here。克隆minimal-performance分支并运行cargo bench将对两种不同的乘法实现进行基准测试(请注意,断言被注释掉以便在该分支中开始)。

另外需要注意的是,如果我更改get*函数以返回数据的副本而不是引用(f64而不是&f64),则效果会消失(但代码是整个过程略慢。

1 个答案:

答案 0 :(得分:2)

这不是一个完整的答案,因为我还没有测试过我的说法,但这可以解释它。无论哪种方式,唯一可以确定的方法是生成LLVM IR和汇编器输出。如果您需要LLVM IR手册,可以在此处找到:http://llvm.org/docs/LangRef.html

无论如何,足够了。我们假设你有这个代码:

#[inline(always)]
pub unsafe fn get_unchecked(&self, row: usize, col: usize) -> &T {
    self.data.get_unchecked(self.row_col_index(row, col))
}

这里的编译器将其更改为间接加载,可能会在紧密循环中进行优化。值得注意的是,每次加载都有可能出错:如果您的数据不可用,它将触发越界。

在边界检查结合紧密循环的情况下,LLVM做了一个小技巧。因为加载是一个紧密的循环(矩阵乘法),并且因为边界检查的结果取决于循环的边界,它将从循环中删除边界检查并将其放在周围环。换句话说,循环本身将保持完全相同,但需要额外的边界检查。

换句话说,代码完全相同,但有一些细微差别。

那么改变了什么?两件事:

  1. 如果我们有额外的界限检查,那么对于超出界限的负载就不再可能了。这可能会触发之前无法实现的优化。不过,考虑到这些检查通常是如何实施的,这不是我的猜测。

  2. 要考虑的另一件事是“不安全”这个词。可能触发某些行为,如附加条件,引脚数据或禁用GC等。我不确定Rust中的这种确切行为;找出这些细节的唯一方法是查看LLVM IR。