每个人都应该了解 SIMD

每个人都应该了解 SIMD

什么是 SIMD 以及它为何有效

SIMD 让 CPU 可以一次对多个数据元素执行一条指令,将按字节处理的循环转变为按块处理的循环,并提供与向量宽度相匹配的线性加速(例如,ARM NEON 为 4×,AVX2 为 8×,AVX‑512 为 16×)。当你经常处理成百上千甚至数百万个数值时,这非常值得尝试。

常见的五步模式

大多数“一次处理 N 个值”的 SIMD 代码遵循五个步骤:将任何需要的常量广播到向量中,按每次一个向量宽度的块进行输入循环,进行逐通道操作,将向量结果归约为标量决策或掩码,最后以处理剩余部分或不支持的 CPU 的标量尾部操作结束。

实际案例:Ghostty 控制字符扫描

在 Ghostty 中,一个用于扫描解码后的码点以查找第一个值 ≤ 0xF 的循环是使用 Zig 语言按照五步模式编写的。代码广播 0xF,加载 u32 向量,对它们进行比较,使用 @reduce 进行归约以查看是否全部大于该值,使用 @bitCast@ctz 找到第一个失败的通道,然后回退到原始的标量循环。这在 ARM NEON 上可实现高达 4× 的吞吐量,在 AVX2 上为 8×,在 AVX‑512 上为 16×,在 AVX2 Intel 桌面电脑上的实际加速约为 5×。

第 1 步:广播常量

结论:你创建一个向量类型并将比较值填充(splat)到每个通道中,以便 CPU 可以一次比较多个值。 Zig 代码片段显示了 const threshold: V = @splat(0xF);,它将 0xF 复制到向量的所有通道中。

第 2 步:一次加载一个向量

结论:按等于向量宽度的块对输入进行循环,每次迭代加载一个完整的向量。 循环条件 while (end + lanes <= cps.len) : (end += lanes) 确保只处理完整的向量,每次加载 lanes 个元素。

第 3 步:执行 SIMD 操作

结论:对整个向量应用操作(例如 >);CPU 在所有通道上并行执行该操作。 表达式 values > threshold 通过单条 CPU 指令生成一个布尔向量,每个通道一个结果。

第 4 步:归约向量结果

结论:结合每个通道的结果(例如使用 @reduce)来决定是继续执行还是使用位技巧定位第一个不匹配项。 @reduce(.And, greater_than_threshold) 用于告知是否每个通道都通过了测试;如果没有,@bitCast 将布尔向量转换为位掩码,~mask 对其取反,@ctz 统计末尾零的个数以找到第一个失败通道的索引。

第 5 步:以标量尾部结束

结论:在向量循环之后,对剩余元素或没有 SIMD 支持的 CPU 运行原始的标量循环。 最后的 while (end < cps.len and cps[end] > 0xF) end += 1; 处理任何剩余部分,同时也作为 simd.lanes(u32) 返回 null 时的回退方案。

为什么编译器并不总是能做到这一点

结论:虽然编译器可以对简单的循环进行自动向量化,但它们经常错过机会且具有不可预测性,因此显式 SIMD 可以提供可靠、可预测的加速。 自动向量化适用于没有复杂控制流的常规算术循环,但生产级编译器经常错过向量化的机会。当一个循环的重要性足以带来 5× 的增益时,你希望向量化是显式的,而不是受到无关代码编辑或编译器更新的无声影响。

每个人都应该了解 SIMD

结论:识别这种模式让你能够决定何时应用 SIMD 而不必担心;这五步模式易于学习,并且在不同语言间具有可移植性。 你不需要编写像 simdutfsimdjson 中那样奇特的算法来获益;一旦你掌握了这些步骤,常见的应用场景会变得简单得多,只需几行代码即可表达。

社区见解与不同观点

结论:Hacker News 的评论者指出,并不总是需要 SIMD,并强调了识别瓶颈、面向数据的设计、宏辅助工具以及了解 SIMD 局限性的价值,即使你依赖编译器或 AI。

  • "并不是每个人都需要了解 SIMD。机械共情(Mechanical sympathy)是软件架构师减少后期返工的重要被动优势,但我认为基准测试和能够识别瓶颈是更重要的日常技能。" – @boricj
  • "我喜欢 SIMD,但在使用 SIMD 等技术对代码进行超优化之前,请务必认真考虑你的数据结构和访问模式。" – @Rendello
  • "每个人都应该了解 SIMD,这样你才能知道何时要求你那位精通此道的挚友 Al 为你在合适的地方使用它。" – @magarnicle
  • "我想我不理解 'educe' 这一步。看起来你必须小心,不要 '抵消' 了 SIMD 带来的所有好处。当然,它可以并行比较 8 个值,但如果你必须逐个查看这 8 个答案,你就回到了原点。示例中的 @reduce() 函数是一个特殊的 Vector 函数,能用一个 '步骤' 告诉你是否所有值都为真吗?" – @losvedir
  • "99% 的开发者应该直接忽略 SIMD。大多数项目都有很多可以提高性能的低垂果实,但仍然没人有时间去解决它们。" – @andix
  • "我学习 SIMD 面临的最大障碍是内联函数(intrinsics)奇怪的命名约定。" – @ktimespi
  • "为了支持这一论点,即使你并不打算亲自编写 SIMD 或打算 '直接让 AI 来做',了解哪些内容在 SIMD 中可以实现快速化也是很重要的..." – @derf_
  • "我的编译器知道 SIMD。然而,了解 SIMD 的局限性可能有助于避免那些无法通过它进行优化的计算。" – @gblargg
  • "我认为如果有处理细节的宏,会有更多的开发者使用 SIMD。" – @taylodl
  • "随着 RISC-V Vector (RVV)(RVA23 的一部分)的出现,这变得比以往任何时候都更容易。" – @snvzz
  • "我会稍微改一下标题为 '每个人都应该知道 SIMD 何时失效'。现代编译器在向量化方面非常出色,直到它们突然不行了,并且它们经常因为 if 假设或单个数据相关的分支而回退到标量代码。学习检查编译器的优化报告可以说更有价值。" – @kiaansaraiya

这些观点强化了本文的建议:学习基础的 SIMD 模式以便识别何时适用,但也要进行测量,考虑数据布局,并在适当的时候依赖编译器或辅助工具。

Sources