Everyone Should Know SIMD

Everyone Should Know SIMD

每个人都应该了解 SIMD

Mitchell Hashimoto | July 22, 2026 Mitchell Hashimoto | 2026年7月22日

SIMD has a reputation for being complex. I’ve met many very good software engineers who dismiss it as something too complex to learn or a niche optimization meant for only the highest-performance software, not useful in everyday programming. I think that’s wrong. SIMD can be simple to understand, and common “process N values at a time” SIMD code to speed up a naive for loop almost always follows the same general shape. Once you learn the basics, writing SIMD is just about as easy as a for loop. And when it’s not, it’s usually a good sign to skip it for now. Every developer should know at least that much SIMD. This post uses Zig for examples but is a general piece that applies to any programming language. Support for SIMD instructions varies by programming language and I hope that more programming languages expose these generic concepts in the future!

SIMD(单指令多数据)一直以复杂著称。我见过许多优秀的软件工程师,他们认为 SIMD 太过复杂,或者只是一种仅适用于高性能软件的利基优化,在日常编程中毫无用处。我认为这种观点是错误的。SIMD 其实很容易理解,而且为了加速简单的 for 循环,常见的“一次处理 N 个值”的 SIMD 代码几乎总是遵循相同的通用模式。一旦掌握了基础知识,编写 SIMD 代码就像写 for 循环一样简单。如果遇到写起来不顺手的情况,那通常是一个信号,说明现在可以先跳过它。每一位开发者至少都应该了解这些 SIMD 基础。本文以 Zig 为例,但其核心思想适用于任何编程语言。目前不同编程语言对 SIMD 指令的支持各不相同,我希望未来有更多的编程语言能提供这些通用的概念接口!

Background: What Is SIMD?

背景:什么是 SIMD?

If you already know what SIMD is, skip this section. SIMD allows a CPU to operate on multiple values in parallel. For example, instead of comparing one byte at a time, a CPU can compare 4, 8, or even more bytes with a single instruction. If you ever see loops like this in your code: for (byte in bytes) { /* ... */ } for (character in string) { /* ... */ } for (value in array) { /* ... */ } There is an opportunity to use SIMD. SIMD turns those into this: for (8 byte chunk in bytes) { /* ... */ }

如果你已经了解 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) { /* ... */ }

This results in a localized speedup that directly maps to the parallelism: you process data 4x, 8x, or even faster. The only real requirement for this to pay off is that you need to be regularly processing a large enough number of bytes. If you’re doing these for loops across data that is only ever a handful or dozens of bytes, it’s not worth it. But if this is iterating over hundreds, thousands, millions of bytes, the payoff will be huge. That’s the basics.

这会带来直接对应并行度的局部加速:你处理数据的速度会提升 4 倍、8 倍甚至更快。要使这种优化产生价值,唯一真正的要求是你需要经常处理足够大量的数据。如果你处理的只是几个或几十个字节的数据,那么使用 SIMD 是不值得的。但如果是在遍历成百上千、数百万字节的数据,收益将是巨大的。这就是基本原理。

The Common Shape

通用模式

The common “process N values at a time” SIMD code follows the same five steps:

  1. Broadcast any constants you need and initialize vector accumulators, if any.
  2. Loop over input one vector-width chunk at a time.
  3. Perform the comparison or arithmetic across all lanes in parallel.
  4. Reduce or store the vector result as needed.
  5. Handle the remaining elements with a scalar tail.

常见的“一次处理 N 个值”的 SIMD 代码遵循相同的五个步骤:

  1. 广播所需的常量,并初始化向量累加器(如果有)。
  2. 以向量宽度为单位遍历输入数据。
  3. 在所有通道(lane)上并行执行比较或算术运算。
  4. 根据需要归约(reduce)或存储向量结果。
  5. 使用标量尾部处理剩余元素。

A scalar tail is just your normal loop from before vectorizing, but it only processes the remainder that doesn’t fit into a full vector. As you do this more and more, you’ll begin to naturally decompose every for loop into these five steps and writing SIMD becomes nearly as natural as writing a scalar loop.

标量尾部就是向量化之前你常用的那种循环,但它只处理那些无法填满一个完整向量的剩余数据。随着练习增多,你会开始自然地将每个 for 循环分解为这五个步骤,编写 SIMD 代码也会变得像编写标量循环一样自然。

A Real Example

实际案例

Let’s look at a real example from Ghostty. We’ll look at the scalar implementation, the SIMD implementation, and then map it back to the common shape above. I have a slice of decoded codepoints that I want to consume until I see a value at or below 0xF (a C0 control character). Terminals are mostly plain characters to be printed, so we try to batch all those together. So this loop finds the end of the next printable run as quickly as possible.

让我们看一个来自 Ghostty 的实际例子。我们将对比标量实现和 SIMD 实现,然后将其映射回上述的通用模式。我有一个已解码代码点的切片,我希望持续处理它,直到遇到一个小于或等于 0xF(C0 控制字符)的值。终端大部分时间都在打印普通字符,因此我们尝试将它们批量处理。这个循环的目标是尽可能快地找到下一个可打印字符序列的末尾。

The scalar loop is one line: while (end < cps.len and cps[end] > 0xF) end += 1; It processes one codepoint at a time. It is easy to understand.

标量循环只有一行: while (end < cps.len and cps[end] > 0xF) end += 1; 它一次处理一个代码点,非常容易理解。

Here is the generic vector version:

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;

这是通用的向量化版本: (代码见上文)

This can improve the loop’s throughput by up to 4x with ARM NEON, 8x with AVX2, and 16x with AVX-512. In real-world end-to-end throughput from terminal program to finalized terminal state on an AVX2 Intel desktop, this was more like a 5x speedup. You always lose some of the ideal speedup due to the other stuff around the SIMD code, but… that’s still 5x!

这可以将循环的吞吐量在 ARM NEON 上提升至 4 倍,在 AVX2 上提升至 8 倍,在 AVX-512 上提升至 16 倍。在 AVX2 Intel 台式机上,从终端程序到最终终端状态的实际端到端吞吐量测试中,速度提升大约为 5 倍。由于 SIMD 代码周围的其他开销,你总是会损失一些理想的加速比,但……那依然是 5 倍的提升!

Step 1: Broadcast Constants

第一步:广播常量

Let’s start with the first three lines:

if (simd.lanes(u32)) |lanes| {
    const V = @Vector(lanes, u32);
    const threshold: V = @splat(0xF);

让我们从前三行开始: (代码见上文)

simd.lanes(u32) is a helper in Ghostty that returns the number of u32 values the target CPU can process at once. These individual values are called lanes. On ARM this returns 4, AVX2 returns 8, and AVX-512 returns 16. If the target doesn’t have a vector size we want to use, it returns null and we skip all of this code and do zero SIMD work. @Vector(lanes, u32) creates the vector type. Finally, we need to compare every value to 0xF. A vector comparison requires a vector on both sides, so @splat(0xF) copies, or broadcasts, 0xF into every lane.

simd.lanes(u32) 是 Ghostty 中的一个辅助函数,返回目标 CPU 一次可以处理的 u32 值数量。这些独立的值被称为“通道”(lanes)。在 ARM 上返回 4,AVX2 返回 8,AVX-512 返回 16。如果目标架构没有我们想要使用的向量大小,它会返回 null,我们就会跳过所有这些代码,不进行任何 SIMD 操作。@Vector(lanes, u32) 用于创建向量类型。最后,我们需要将每个值与 0xF 进行比较。向量比较要求两侧都是向量,因此 @splat(0xF) 会将 0xF 复制或广播到每一个通道中。