gpt4 book ai didi

performance - 为什么删除边界检查后我的代码运行速度变慢了?

转载 作者:行者123 更新时间:2023-11-29 07:40:49 24 4
gpt4 key购买 nike

我正在用 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 .感兴趣的函数是:

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

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

更新:

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

还需要注意的是,如果我更改 get* 函数以返回数据副本而不是引用(f64 而不是 &f64 ),效果消失了(但代码整体上稍微慢了一点)。

最佳答案

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

关于performance - 为什么删除边界检查后我的代码运行速度变慢了?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37514909/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com