4 个版本
0.2.1 | 2023年2月10日 |
---|---|
0.2.0 | 2021年3月27日 |
0.1.1 | 2020年9月21日 |
0.1.0 | 2020年6月23日 |
#404 in 硬件支持
245 每月下载量
98KB
1.5K SLoC
Slipstream
这个库帮助以更好地激励编译器优化结果的方式编写代码(实际上并没有做任何事情)。
现代编译器,包括 rustc
,能够通过使用循环展开和自动向量化等技术来提高结果的性能,通常优于手工编写的代码。尽管如此,每种优化都需要一些假设,在应用之前必须证明这些假设成立。
该库提供了“向量”类型,如 u16x8
,它们的行为与小的固定大小数组非常相似(在这种情况下,将是 [u16; 8]
),但为它们定义了算术运算。它们还强制整个向量的对齐。因此,可以以这种方式编写算法,以便在数据组上工作,并使编译器更容易证明假设。这可以通过向编译器“免费”提供这些证明,并允许它应用激进的优化来实现多倍速度提升。
API 启发于 packed_simd
和 faster
crate,但它依赖于自动向量化器而不是使用显式的 SIMD 指令,因此在稳定版 Rust 上也能工作,甚至在没有显式 SIMD 支持的平台(或完全没有 SIMD 支持)上也能实现速度提升。
缺点是优化不能保证。虽然它往往能产生与手工编写的向量化代码相竞争甚至更好的结果,但周围代码的微小变化也可能导致结果严重变差。建议只将其应用于紧密循环,循环中数据足够多以便进行压缩,并测量性能。
它与函数多版本化很好地配合使用,例如查看 multiversion
crate。
更多详细信息可以在 文档 中找到,包括有效使用技巧以及如果性能不如预期时应该尝试的内容。
示例
作为一个非常简单的例子,想象一下应用程序性能的核心是求和一大组浮点数,我们有了以下代码
fn compute(d: &[f32]) -> f32 {
d.iter().sum()
}
现在,可以通过手动向量化将其改写为类似以下内容
use core::arch::x86_64 as arch;
unsafe fn compute_sse(d: &[f32]) -> f32 {
let mut result = arch::_mm_setzero_ps();
let iter = data.chunks_exact(4);
let remainder = iter.remainder().iter().sum::<f32>();
for v in iter {
result = arch::_mm_add_ps(result, arch::_mm_loadu_ps(v.as_ptr()));
}
let result: [f32; 4] = mem::transmute(result);
let result = result.iter().sum::<f32>() + remainder;
}
虽然这样做确实能显著提高速度,但它可读性较差,需要在应用程序逻辑中允许使用不安全操作,并且不兼容(它不会在非英特尔处理器上运行,即使在那里也不会利用更新和更好的向量指令)。这些缺点通常使得它对于更复杂的算法不值得追求。
使用slipstream
,也可以这样编写
fn compute_slipstream(d: &[f32]) -> f32 {
// Will split the data into vectors of 4 lanes, padding the last one with
// the lanes from the provided parameter.
d.vectorize_pad(f32x4::default())
// Sum the vectors into a final vector
.sum::<f32x4>()
// Sum the lanes of the vectors together.
.horizontal_sum()
}
这仍然比原始版本长且复杂,但似乎比手动版本更容易管理。它也是可移植的,可能在没有向量指令的平台上有一些速度提升。通过在函数上使用正确的注解,还可以生成多个版本,并在运行时调度利用CPU支持的最新和最亮丽指令的那个版本。
在i5-8265U上的相应基准测试表明,这个版本接近手动版本。确实,还有类似的变体,速度甚至更快。
test sum::basic ... bench: 11,707,693 ns/iter (+/- 261,428)
test sum::manual_sse_convert ... bench: 3,000,906 ns/iter (+/- 535,041)
test sum::vectorize_pad_default ... bench: 3,141,834 ns/iter (+/- 81,376)
注意:要重新运行上述基准测试,请在type V = f32x4
中在benches/utils.rs
使用。
警告:浮点数不满足结合律。第一个、手动的版本可能会因为舍入误差而产生略有不同的结果。
需要帮助
这是一个开源库,欢迎为其开发提供帮助。有些地方您的贡献将特别受到欢迎
- 关于API、文档以及整体可用性的反馈。
- 实现缺失的API:虽然已经涵盖了大部分内容,但仍有一些领域尚未涵盖。我知道有
- 一些方法可以在不同大小的基类型之间进行转换(例如,
f32x4 -> f64x4
)。 - 基类型上存在的一些方法——浮点数的三角函数、舍入、绝对值、无符号整数的位设置/清除...
- 向量-标量乘法。目前可以执行例如
f32x2::splat(-1.0) * f32x2::new([1, 2])
,但如果可以简单地写成-1.0 * f32x2::new([1, 2])
,将会更方便。
- 一些方法可以在不同大小的基类型之间进行转换(例如,
- 用例和基准测试:如果你能提出一个简单、易于矢量化的问题并将其作为基准测试提交,这将有助于保持和提升库的性能。无论是库表现良好还是表现不佳的情况(后者可能被视为一种类型的错误)都是有益的。最佳情况下,如果这样的基准测试包含一个朴素实现(不使用此库)、使用此库的实现(可能包含多个变体)以及带有平台特定内建的手动编写的矢量化代码。但如果其中任何一项缺失(例如,由于手动编写矢量化代码的工作量过大),也比没有好。
- 提升性能:虽然编译器使程序运行得更快,但编译器在完成这项任务时的好坏很大程度上取决于它是否能够“看穿”代码。如果你能够以更易于理解和透明的方式调整某些方法的实现,那就太好了。大多数代码都尽可能地编写得很快,目前只进行了一些微调。例如,
vectorize_pad
方法似乎速度惊人地慢,理想情况下它应该产生与vectorize
相当速度的代码。 - 处理不安全代码:在许多地方,该库使用了
unsafe
代码。这通常是由于性能考虑——例如,从迭代器初始化GenericArray
阻止了许多优化,并导致性能显著下降。最佳情况下,每个此类unsafe
代码都将被安全代码替换,或者将包含注释来解释/证明其确实安全。
如果你想要从事更大的项目,打开一个仓库问题是一个好主意,以便首先讨论它并保留这个任务。
许可证
许可方式如下
- Apache License,版本2.0,(LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT许可证(LICENSE-MIT 或 http://opensource.org/licenses/MIT)
任选其一。
贡献
除非你明确表示,否则根据Apache-2.0许可证定义的,你有意提交的任何旨在包含在你所做工作的贡献,都应按上述方式双重许可,不附加任何额外条款或条件。
依赖项
~155KB