【问题标题】:Why does my code run slower when I remove bounds checks?当我删除边界检查时,为什么我的代码运行速度变慢?
【发布时间】:2016-09-27 15:34:04
【问题描述】:

我正在用 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 而不是 &amp;f64),效果就会消失(但代码会稍微慢一点)。

【问题讨论】:

  • 老问题又来了:您是否使用编译器优化进行编译(带有--release 标志)? ;)
  • 对生锈一无所知:您的基准测试是否合理?缓存效果、方差、测试数据同步...
  • @LukasKalbertodt 是的,我使用cargo bench 运行我的基准测试,它会随着发布自动编译。
  • 当你检查边界时,也许编译器可以更积极地优化?
  • 对于类似的东西,最好在发布模式下编译两个独立的二进制文件,并使用objdump -D 来识别相关紧密循环中使用的机器指令。

标签: performance optimization rust llvm-codegen


【解决方案1】:

这不是一个完整的答案,因为我没有测试我的声明,但这可能会解释它。无论哪种方式,唯一确定的方法是生成 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。

【讨论】:

    猜你喜欢
    • 2012-01-02
    • 1970-01-01
    • 1970-01-01
    • 2016-10-23
    • 2021-08-02
    • 2018-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多