每个人都该懂 SIMD
文章摘要
Mitchell Hashimoto(HashiCorp 创始人、Ghostty 终端模拟器作者)在这篇文章里提出一个主张:SIMD(单指令多数据)并没有开发者想象中那么复杂,它应该成为每个程序员的基础知识之一。他的核心论点是,常见的向量化模式其实遵循一个可预测的固定结构,练习几次之后就会变成直觉。
他把典型 SIMD 代码的实现总结为五个一致的步骤:一,把常量广播(broadcast)成向量形式;二,按向量宽度分块循环,而不是逐个元素遍历;三,在所有 lane 上并行执行运算;四,把向量结果归约(reduce)成可用的输出;五,用标量代码处理余下不足一个向量宽度的”尾巴”(tail)。
举的例子来自 Ghostty 的真实代码:扫描一串码点(codepoint)以定位控制字符。标量版本只是一行循环;向量化版本则视 CPU 架构(ARM NEON、AVX2 或 AVX-512)一次并行处理 4 到 16 个值,理论上可获得 4 倍、8 倍甚至 16 倍加速,实测在终端操作中约为 5 倍。代码细节涵盖向量类型的创建与阈值广播、跨 lane 的并行比较、用位转换(bit-cast)加尾部零计数(trailing-zero count)定位第一个比较失败的位置,以及对余数和不支持架构的标量回退路径。
文中一个重要的立场是:为什么不干脆依赖编译器自动向量化。Hashimoto 明确指出,编译器很少能可靠地自动向量化复杂情况,他自己文中那个例子在 LLVM 和 GCC 的最高优化等级下都无法被自动向量化——据他所知,带有提前 break 的循环编译器基本永远不会向量化。更重要的理由是可预测性:”当这个循环重要到我在乎 5 倍加速时,我希望向量化是显式且可预测的。我不想让一次不相关的代码改动或编译器升级悄悄把它变回标量循环。”文章结构从定义背景讲到通用模式框架,再到真实案例逐步对应五个步骤,然后论证手写向量化优于依赖编译器优化,最后呼吁开发者普遍具备 SIMD 素养。全文的适用边界也很清楚:SIMD 擅长处理成百上千乃至上百万个连续的数据元素。
HN 评论精华
- qurren:开场就是全场最具争议的一条——”我直接
gcc -O3就能拿到 SIMD,不用去学它。” - forrestthewoods:自动向量化远不如你期望的那么好。”最好的 SIMD 优化很可能需要把你的数据格式从 AoS 改成 SoA。”(Array of Structs 改成 Struct of Arrays。)
- mitchellh(作者):现场提供实证——”我自己文章里那个例子在 LLVM 和 GCC 最高优化等级下都不会被自动向量化。基本上,据我所知编译器永远不会向量化带提前 break 的循环。”
- abbeyj:给出了反例和最新进展——你需要给编译器一些帮助,比如用
&而不是and来避免短路求值,这样 16 个元素每轮都会被读取,编译器就有自由把它们替换成一次 16 字节加载。另外”编译器永远不会向量化带 break 的循环”这一点正在改变:GCC 14 不会,但 GCC 15 会,这一项还被写进了 GCC 15 发布说明的”通用改进”章节;不过 clang/LLVM 目前没有等价能力。 - spider-mario:引用 ispc 作者 Matt Pharr 的经典论述作为反驳——”自动向量化不是一种编程模型”。问题在于只要向量化有可能失败(而它一定会失败),那么真正在乎生成代码的程序员就必须深入理解自动向量化器,然后在它失灵时用各种方式去戳它或改自己的程序。”这是一种糟糕的编程方式,全是炼金术和猜测,你还得对某一个编译器实现的细枝末节变成专家。等他们发布新版本改了自动向量化器实现,愿上帝保佑你。”
- spider-mario(续):给出替代方案——用 Google 的 Highway 这类库,它还能让你在一个二进制里检测并利用 AVX2 或 AVX-512,同时仍能在不支持这些指令集的 CPU 上运行;而依赖自动向量化就得用
-mavx2、-mavx512编译,”那基本上没有任何依赖预编译二进制的用户能享受到”。 - llm_nerd:立场最强硬的反方——”HN 对 SIMD 有种恋物癖,但如果你在手写 SIMD 而又不是在写一个显式的加速库,你就是做错了。100% 的情况下都是。”他认为每个现代语言都有向量化优化编译器,通过一些相当直白的技巧就能自动搞定。saagarjha 回应:”编译器确实很好,但’很好’在你真正需要 SIMD 的场合并没什么用。”
- jandrewrogers:给出了原理层面的解释——大多数从标量到 SIMD 的转换需要改变数据结构和算法的设计才能有效,而编译器出于显而易见的原因必须严格、确定性地复现你指定的数据结构。”即使编译器聪明到能为 SIMD 变换你的数据结构和算法(它们不能),数据结构本身也是一份不能被单方面修改的契约。”
- derf_:为文章的论点加码——即便你不打算自己写 SIMD、或者打算”让 AI 写”,知道什么在 SIMD 里能快、在什么硬件上能快,依然很重要,因为这让你能把算法和代码结构设计成 SIMD 可行的样子。诸如数据依赖为何重要、加宽向量元素有多贵(以及如何避免)、如何把条件和分支转成掩码,甚至”除法根本不存在”这类事实,都在你亲手用过 SIMD 之后才容易内化。
- tstack:补充边界条件——如果输入是一大段数据一次性检查/变换,SIMD 效果很好;但如果你很可能要基于输入中的若干字节做决策,SIMD 会和标量一样快或更慢。”它不是一个魔法’变快’按钮。”他还以 simdjson 为例反驳”simdjson 证明了 SIMD 万能”的说法:仔细看会发现它的高性能主要来自跳过大块输入数据,”JSON 结构几乎全由单字节构成(
{ } [ ] , : "),如果你不跳过东西,你就躲不开必须检查这些字节并对它们分支”。 - Rendello:整个讨论中最实用的一条——在用 SIMD 之类手段超级优化之前,请认真考虑你的数据结构和访问模式。他曾在 Zig 里玩 SIMD,但”我建模数据结构的方式与优化如此对立,就像给一辆发动机坏了的柠檬车装上高性能赛车轮胎”。他现在把数据当成 SQL 表来建模,先想清楚潜在的”主键”是什么,再围绕访问模式设计结构。他给了树的具体例子:以前用结构体指向堆上其他结构体,结果同时具备链表的所有坏特性、多个堆 vector 的碎片化,以及糟糕的建立/销毁时间(光 Drop 就占了可观的运行时);但树可以有无数种表示方式,总是能线性化。他还整理了同一概念的几种叫法:AoS vs SoA、行主序 vs 列主序、行式 vs 列式(数据库领域)。
- Rendello(推荐资料):面向数据设计(Data-Oriented Design)的两场最著名演讲——游戏引擎开发者 Mike Acton 的《Data-Oriented Design and C++》和 Zig 主创 Andrew Kelley 的《A Practical Guide to Applying Data Oriented Design》,以及 Richard Fabian 的 DoD 书。他说读那本书时一开始被里面大谈数据库表设计弄糊涂了(一本讲高性能 C++ 游戏引擎的书?),但一旦想通就非常震撼:重点是认真思考访问模式以及什么可以充当好的”主键”。
- saagarjha:给数据导向热潮泼了盆冷水——”反过来说,我作为性能工程师的很多工作是在收拾那些读了两篇 SoA 博客就断定封装很蠢的人留下的糟糕抽象。”
- luaKmua:作为性能工程师的”永恒战争”——”性能始于架构,数据布局差的热路径你能榨出的东西是有限的。好处是面向数据的代码几乎总能轻松支持多线程和 SIMD。”
- morganherlocker:指出被忽视的机会——把分配次数从每次运行数百万降到初始化时的几个,本身就是一个”变快按钮”,而且通常更可靠。”团队常常花大力气靠消除分支给分配密集的代码提速 2-3 倍,而如果把重构分配也纳入考虑范围,其实能拿到 100-1000 倍的提速(这还是在手工调优 SIMD 那额外 4-8 倍之前)。”
- wrl:详细的 Zig SIMD 实战体验——有些 builtin 声称能作用于 SIMD 向量,实际却是把向量拆开逐元素处理(比如对
@Vector(4, f32)调@sin()会拆成 4 次标量@sin()再打包回去);std.math大部分只支持标量;还缺一些他习惯从xmmintrin.h拿到的 intrinsic(rcp、rsqrt 等)。他觉得 Zig 有”不做意料之外的隐藏代码执行”的强硬立场,所以看到一个”披着风衣的一堆标量函数”很意外,”也许不真正支持向量执行的函数干脆就不该接受向量参数”。 - mitchellh(回应为什么 Ghostty 用 C++ 库做 SIMD):”Zig 向量的主要局限是它们只在编译期。所以如果你在构建面向基线 CPU 目标编译的可再分发软件,它就不会针对你那台机器做到最优。”Highway 会为不同硬件配置各编译一份 SIMD 模块,启动时做 CPUID 指纹识别决定加载哪个,”这样即便是基线版本也带有 AVX-512 等实现,我们只是在运行时激活正确的那个”。他们只对最热的热路径用 Highway。他还透露:”我花了好几百美元,靠这条好狗 GPT 把它 slop-fork 成了 Zig,实际效果很好,但我不想维护它。”
- pton_xd:引用文章中关于”显式且可预测”的那段,指出这正是为 V8 这类 JIT 编译器优化代码时最痛苦的部分——”在别处把一个常量从 1 改成 1.0,都可能改变所执行的优化并导致意外的性能下降”。
- hnal943 / crabmusket:推荐 Casey Muratori 讲《The Witness》开发团队如何用 SIMD 解决具体性能问题的视频。crabmusket 认为这是”为性能而垂直整合”的绝佳例子——看完你会理解为什么那些抽象存在、为什么它们必须那么通用,但当你有具体用例时,就能从问题定义一路垂直整合到 SIMD 并获得巨大回报。