Everyone Should Know SIMD

WadeGrimridge2 pts0 comments

Everyone Should Know SIMD – Mitchell Hashimoto<br>Mitchell Hashimoto

Mitchell Hashimoto<br>Everyone Should Know SIMD<br>July 22, 2026<br>SIMD<br>has a reputation for being complex. I've met many very good software engineers<br>who dismiss it as something too complex to learn or a niche optimization<br>meant for only the highest-performance software, not useful in everyday<br>programming.

I think that's wrong. SIMD can be simple to understand1, and common<br>"process N values at a time" SIMD code to speed up a naive for loop<br>almost always follows the same general shape. Once you learn the basics,<br>writing SIMD is just about as easy as a for loop. And when it's not, it's<br>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<br>applies to any programming language. Support for SIMD instructions<br>varies by programming language and I hope that more programming languages<br>expose these generic concepts in the future!

I hate that I have to do this for every post now, but I also want<br>to note this was completely hand-written with no AI assistance.

Table of ContentsBackground: What Is SIMD?<br>The Common Shape<br>A Real Example<br>Step 1: Broadcast Constants<br>Step 2: Loop One Vector at a Time<br>Step 3: Perform the SIMD Operation<br>Step 4: Reduce the Vector Result<br>Step 5: Finish with the Scalar Tail<br>Recap: The Common Shape<br>Why Can't the Compiler Do This?<br>Everyone Should Know SIMD

Background: What Is SIMD?

If you already know what SIMD is, skip this section.

SIMD<br>allows a CPU to operate on multiple values in parallel. For example,<br>instead of comparing one byte at a time, a CPU can compare<br>4, 8, or even more bytes with a single instruction.

If you ever see loops like this in your code:

for (byte in bytes) { /* ... */ }<br>for (character in string) { /* ... */ }<br>for (value in array) { /* ... */ }

There is an opportunity to use SIMD. SIMD turns those into this:

for (8 byte chunk in bytes) { /* ... */ }

This results in a localized speedup that directly maps to the parallelism:<br>you process data 4x, 8x, or even faster.

The only real requirement for this to pay off is that you need to be<br>regularly processing a large enough number of bytes. If you're doing<br>these for loops across data that is only ever a handful or dozens of bytes,<br>it's not worth it. But if this is iterating over hundreds, thousands,<br>millions of bytes, the payoff will be huge.

That's the basics. Projects such as<br>simdutf and<br>simdjson<br>take this to an extreme and use SIMD techniques that can<br>be difficult to understand. But you do not need to write algorithms like<br>those to benefit from SIMD. The common case is dramatically simpler.

The Common Shape

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

Broadcast any constants you need and initialize vector accumulators, if any.

Loop over input one vector-width chunk at a time.

Perform the comparison or arithmetic across all lanes in parallel.

Reduce or store the vector result as needed.

Handle the remaining elements with a scalar tail. A scalar tail is just your<br>normal loop from before vectorizing, but it only processes the remainder<br>that doesn't fit into a full vector.

As you do this more and more, you'll begin to naturally decompose every<br>for loop into these five steps and writing SIMD becomes nearly as natural<br>as writing a scalar loop.

A Real Example

Let's look at a real example from Ghostty. We'll look at the scalar<br>implementation, the SIMD implementation, and then map it back to the<br>common shape above.

I have a slice of decoded codepoints that I want to consume until I see a value<br>at or below 0xF (a C0 control character).2 Terminals are mostly<br>plain characters to be printed, so we try to batch all those together.<br>So this loop finds the end of the next printable run as quickly as possible.

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.

Here is the generic vector version with no CPU-specific intrinsics3<br>and no comments. I will explain it in detail later.

if (simd.lanes(u32)) |lanes| {<br>const V = @Vector(lanes, u32);<br>const threshold: V = @splat(0xF);<br>while (end + lanes cps.len) : (end += lanes) {<br>const values: V = cps[end..][0..lanes].*;<br>const greater_than_threshold = values > threshold;<br>if (@reduce(.And, greater_than_threshold)) continue;<br>const mask: std.meta.Int(.unsigned, lanes) = @bitCast(greater_than_threshold);<br>end += @ctz(~mask);<br>break;

while (end cps.len and cps[end] > 0xF) end += 1;

12 more lines of code.

This gets a throughput speedup of 4x on ARM CPUs (e.g. most Macs nowadays)<br>and 8x on Intel CPUs (AVX2, almost all x86 devices today) and 16x on<br>Intel workstations (AVX-512, rarer, mostly high-end servers and workstations).

In real-world end-to-end throughput from terminal program to finalized<br>terminal state on an AVX2 Intel desktop, this was more like a 5x speedup.<br>You always lose some of the ideal speedup...

simd loop vector lanes common know

Related Articles