我正在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
),则效果会消失(但代码是整个过程略慢。
答案 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做了一个小技巧。因为加载是一个紧密的循环(矩阵乘法),并且因为边界检查的结果取决于循环的边界,它将从循环中删除边界检查并将其放在周围环。换句话说,循环本身将保持完全相同,但需要额外的边界检查。
换句话说,代码完全相同,但有一些细微差别。
那么改变了什么?两件事:
如果我们有额外的界限检查,那么对于超出界限的负载就不再可能了。这可能会触发之前无法实现的优化。不过,考虑到这些检查通常是如何实施的,这不是我的猜测。
要考虑的另一件事是“不安全”这个词。可能触发某些行为,如附加条件,引脚数据或禁用GC等。我不确定Rust中的这种确切行为;找出这些细节的唯一方法是查看LLVM IR。