返回

文章详情

每个人都应该了解 SIMD

Hacker News2026年7月22日 17:48

SIMD 以其复杂性而闻名。我遇到过许多优秀的软件工程师,他们将其视为一种过于复杂以至于无法学习的东西,或者是一种仅适用于最高性能软件的利基优化,而在日常编程中没有用处。我认为这是错误的。SIMD 可以很简单地理解,常见的 "一次处理 N 个值" 的 SIMD 代码几乎总是遵循相同的一般形状。一旦你掌握了基础,编写 SIMD 代码就与编写 for 循环一样容易。而当它不那么简单时,通常是一个跳过它的好迹象。每个开发者至少应该了解这些 SIMD 的基本知识。这篇文章使用 Zig 作为例子,但它是一个适用于任何编程语言的一般性文章。不同编程语言对 SIMD 指令的支持各不相同,我希望未来会有更多的编程语言能揭示这些通用概念!我讨厌我现在每篇文章都必须这样做,但我也想说明这完全是手写的,没有任何 AI 协助。 目录 背景:什么是 SIMD? 常见形状 一个真实的例子 步骤 1:广播常量 步骤 2:逐个向量循环 步骤 3:执行 SIMD 操作 步骤 4:减少向量结果 步骤 5:结束时的标量尾部 回顾:常见形状 编译器为什么不能做到这一点? 每个人都应该了解 SIMD 背景:什么是 SIMD? 如果你已经知道 SIMD 是什么,可以跳过此部分。SIMD 允许 CPU 并行操作多个值。例如,CPU 可以在一条指令中比较 4、8 或更多字节,而不是一次比较一个字节。如果你在代码中看到类似下面的循环:for ( byte in bytes ) { /* ... */ } for ( character in string ) { /* ... */ } for ( value in array ) { /* ... */ } 那么就可以使用 SIMD。SIMD 将它们转化为:for ( 8 byte chunk in bytes ) { /* ... */ } 这会在局部加速上直接映射到并行性:你以 4 倍、8 倍甚至更快的速度处理数据。真正的要求是你需要定期处理足够数量的字节。如果你对只有十几个字节的数据进行这些 for 循环,那就没有必要。但如果这是在对数百、数千、数百万字节进行迭代,收益将是巨大的。这就是基本知识。像 simdutf 和 simdjson 这样的项目将这一点推向极限,使用的 SIMD 技术可能难以理解。但是你不需要像那些项目一样编写算法就能从 SIMD 中受益。常见情况要简单得多。 常见形状 常见的 "一次处理 N 个值" 的 SIMD 代码遵循相同的五个步骤:广播你需要的任何常量并初始化向量累加器(如果有的话)。一次处理一个向量宽度的数据块。对所有通道进行比较或算术运算。根据需要减少或存储向量结果。处理剩余元素时使用标量尾部。标量尾部只是你在向量化之前的正常循环,但它只处理不适合完整向量的剩余部分。当你越来越多地进行这个操作时,你会自然而然地将每个 for 循环分解为这五个步骤,编写 SIMD 将变得几乎像编写标量循环一样自然。 一个真实的例子 让我们来看一个来自 Ghostty 的真实例子。我们将查看标量实现、SIMD 实现,然后将其映射回上面的常见形状。我有一个已解码代码点的切片,我想处理,直到看到一个值为 0xF 或更低(一个 C0 控制字符)。终端大多数是普通字符,因此我们尝试将所有这些字符批处理在一起。因此,这个循环尽可能快地找到下一个可打印序列的结束。标量循环只有一行:while (end < cps.len and cps[end] > 0xF) end += 1; 这一次处理一个代码点。很容易理解。这里是没有 CPU 特定内在的通用向量版本,没有评论。我稍后会详细解释。if (simd.lanes(u32)) | lanes | { const V = @Vector(lanes, u32); const threshold: V = @splat(0xF); while (end + lanes <= cps.len): (end += lanes) { const values: V = cps[end .. ][0 .. lanes]. *; const greater_than_threshold = values > threshold; if (@reduce( . And, greater_than_threshold)) continue; const mask: std.meta.Int( . unsigned, lanes) = @bitCast(greater_than_threshold); end += @ctz(~ mask); break; } } while (end < cps.len and cps[end] > 0xF) end += 1; 12 行代码。使用 ARM NEON(包括 Apple Silicon)可以将循环的吞吐量提高多达 4 倍,使用 AVX2(大多数现代 x86 CPU)可以提高 8 倍,使用 AVX-512(一些 Intel CPU 和 AMD Zen 4 及更高版本)可以提高 16 倍。在实际的端到端吞吐量中,在 AVX2 Intel 桌面上从终端程序到最终终端状态,这个提升更像是 5 倍。你总是会因 SIMD 代码周围的其他内容而失去一些理想的加速,但……这仍然是 5 倍!好的,现在我明白这 12 行...

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡