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 硬件支持

Download history 32/week @ 2024-04-22 62/week @ 2024-05-06 34/week @ 2024-05-13 107/week @ 2024-05-27 69/week @ 2024-06-03 28/week @ 2024-06-10 117/week @ 2024-06-17 103/week @ 2024-06-24 90/week @ 2024-07-01 162/week @ 2024-07-08 78/week @ 2024-07-15 113/week @ 2024-07-22 42/week @ 2024-08-05

245 每月下载量

Apache-2.0 OR MIT

98KB
1.5K SLoC

Slipstream

Actions Status codecov docs

这个库帮助以更好地激励编译器优化结果的方式编写代码(实际上并没有做任何事情)。

现代编译器,包括 rustc,能够通过使用循环展开和自动向量化等技术来提高结果的性能,通常优于手工编写的代码。尽管如此,每种优化都需要一些假设,在应用之前必须证明这些假设成立。

该库提供了“向量”类型,如 u16x8,它们的行为与小的固定大小数组非常相似(在这种情况下,将是 [u16; 8]),但为它们定义了算术运算。它们还强制整个向量的对齐。因此,可以以这种方式编写算法,以便在数据组上工作,并使编译器更容易证明假设。这可以通过向编译器“免费”提供这些证明,并允许它应用激进的优化来实现多倍速度提升。

API 启发于 packed_simdfaster 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-2.0许可证定义的,你有意提交的任何旨在包含在你所做工作的贡献,都应按上述方式双重许可,不附加任何额外条款或条件。

依赖项

~155KB