Good article!<p>I just wouldn't start off with bold sentences as<p>> SIMD can be simple to understand<p>and<p>> writing SIMD is just about as easy as a for loop<p>and then the first example requires 12 lines to replace one line of scalar code.<p>Be honest and say SIMD <i>is</i> hard but the results are worth it!<p>(Another nitpick: if this article is for newbies, don't use SIMD-only words and concpts before explaining them. Step 5 is good: scalar tails are mentioned and described. Step 1 is bad: nobody is supposed to know what broadcast mean.)
I had the same thoughts about SIMD code being too verbose when I wrote some, so a few months ago I tried writing a library that lets you write quasi-GLSL code in C++, so much more compact, with the ability to switch between SIMD width without having to rewrite anything at all:<p><a href="https://github.com/gitdepierre/cppshader" rel="nofollow">https://github.com/gitdepierre/cppshader</a><p>Not sure it will ever be useful, but it was a fun pet project with some interesting problems to solve.
>> writing SIMD is just about as easy as a for loop<p>> and then the first example requires 12 lines to replace one line of scalar code.<p>> Be honest and say SIMD is hard but the results are worth it!<p>I think SIMD, and certainly that first example is way more <i>tedious</i> than it is <i>hard</i>.<p>What makes it tedious are<p>- you have to figure out how many things your hardware can do in parallel<p>- you have to chop up the work in packets of that size<p>- once you have your results, you have to ‘unchop’ them<p>- if there is a chance ‘chopping up’ leaves you with remaining items, you have to handle that case separately<p>- if the code you want to apply SIMD to uses constants, you have to create vectors containing copies of them<p>Neither of those is particularly hard, but each adds work, making things tedious.
I hate to be the one to invoke AI in this otherwise virgin thread, but AI is absolutely terrific for doing the not-hard, tedious work. This seems like a place where AI tools could be used to help the user learn - so long as he instructs the tool for each small task and doesn't lazily just have the tool do the thinking for him.
Around 1990 I had the fortune to learn Parallel-C - a language that was designed during the Transputer hype and was essentially C extended by a few features to support easy parallel programming.<p>My favourite feature was<p><pre><code> par(<start expression>; <end expression>; <loop expression>)
</code></pre>
essentially a for loop that will be auto-parallelized by the compiler - some boundary conditions apply.
I believe this still lives on through XMOS. I remember (fondly) doing this on one of their chips mid-to-late 2000's doing a bit of audio processing. Think it might have even been designed by the original transputer folks.
You may be interested in OpenMP.
Agreed, I was interested and I'm prob the target audience but things escalated too fast too quickly, very similar to the infamous "how to draw an owl" meme.
SIMD <i>is</i> simple, using data-parallel operations in scalar languages is what's awkward.
This is probably one of the biggest sins in technological teaching. Sure it is crucially important to take away the fear of a topic. But you don't do so by <i>saying</i> it is simple, you do so by <i>showing</i> it is simple.<p>And it turns out sometimes you cannot show it is simple, because it is in fact very complex. But every complex topic is made up of smaller, simpler ones. Good teachers then manage to find a good order of those smaller parts that makes the steep hill climbable. Then you only need to convince people it is actually worth climbing.
Yep, I avoid saying the word simple almost entirely - its straight forward to get to the top of a mountain, it might still be incredibly arduous.
True on all counts. Not sure why you're getting downvoted.
It's all relative though. The rules of playing bridge (the card game) are much harder than understanding the rules around SIMD instructions.
> Okay, now I understand that those 12 lines are going to look really alien to someone not familiar with the concepts. So now let's back up and explain it step by step, mapping it directly to the shape previously mentioned.<p>They covered that, in a very honest and blunt manner.
It's not a nitpick. The undefined acronym problem strikes yet again. It is fantastic to never use to AI write your blog posts, but please at least have it read it once. It's literally free to catch these simple writing errors and improve your writing. Vis-a-vis:<p><pre><code> Everyone Should Know AI
AI 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 meant for only highest-performance, not useful everyday. I think that's wrong. AI can be simple to understand, and common AI editorializing can speed up a blog, and almost always follows the same general shape. Once you learn the basics, editorializing with AI is just easy. And when it's not, it's usually a good sign to skip it for now. Every developer should know at least that much AI.</code></pre>
The lines in SIMD are much simpler though. It's like replacing one line of "I have a desire for yellow nourishment from the tropics" to "give banana" 12x
I think an even better advice is that everyone should know array programming, because you generally need that mindset for SIMD optimizations as (packed) SIMD-specific techniques are surprisingly rare. And array programming gives you a generally performant code even without SIMD because it is much easier to auto-vectorize.
I'm no fan of closed-source languages, and lord knows MATLAB has its warts. But I can't deny that it was pretty seamless to write efficient vectorised code for numerical simulations at uni. I don't have much experience with it, but my understanding is that Julia is the closest thing to a more modern and expressive language that has similar vectorisation capabilities.
Array programming where we compare first and look for the first failure later will not help much here if runs are short because by itself it doesn't give you early termination and you may spend a lot of time on wasted comparisons.
The last few days I've been using AVX-512 to optimize matrix operations in a bioinformatics project, and it's great! The bottleneck in most applications is reading the large dataset from memory, so rather than doing it multiple times to compute multiple operations you can do everything in one pass (fused kernel) with AVX registers. 5x speedups are quite common. I've been doing it with manual intrinsics, but the wide crate also makes common operations completely trivial. Highly recommend checking it out.<p><a href="https://docs.rs/wide/latest/wide/" rel="nofollow">https://docs.rs/wide/latest/wide/</a>
I'd slightly rephrase the title to "everyone should know when SIMD didn't happen." Modern compliers are extremely good at vectorization until they suddenly aren't, an they'll often fall back to scalar code because if assumptions or a single-data dependent branch. Learning to check the compliers optimization reports is arguably more valuable.
> Learning to check the compliers optimization reports is arguably more valuable<p>Where do I start?<p>I want to trust the compiler, but I don't always have time to feed every little piece into compiler explorer and interpret it. Is there a higher-level workflow?
> Learning to check the compliers optimization reports is arguably more valuable.<p>More valuable than learning to write SIMD code? When writing SIMD code is itself the remedy to lousy autovectorisation?<p>If you can only identify the problem, you’re left with “well, that sucks”.
That’s exactly what happened:<p><a href="https://xcancel.com/mitchellh/status/2079672171321081908#m" rel="nofollow">https://xcancel.com/mitchellh/status/2079672171321081908#m</a>
You know, it's always funny to read takes like "A broken compiler forcing you to write explicit SIMD instead of trusting auto-vectorization and coming out 20-30% faster is the best argument I've seen for reading your own generated assembly occasionally instead of assuming the compiler has you covered" because you can quite easily imagine an alternative one like "A broken compiler revealing that the auto-vectorization actually already accounts for 50% of total speed up of O3, and manual reimplementation and code restructuring provided only additional 20% in some scenarios is the best argument I've seen for almost never bothering with hand-crafting assembly anymore".
Explicit SIMD doesn’t have to mean hand-crafting assembly (and I don’t think it did in this instance).
Is there some way to write unit tests for cases where you know vectorisation should have been applied? I guess micro benchmarks should cover the performance part. We have ArchUnit to cover code structures, it would be nice if something similar exists for generated assembly.
Yeah it's strange how they brushed away that the compiler reached 77% of the hand crafted performance without even trying.
To bolster the argument, even if you do not plan to write the SIMD yourself or will "just get AI to do it", it is important to know <i>what can be fast</i> in SIMD (and on what hardware). That allows you to design your algorithms and structure your code so that the SIMD is possible.<p>Internalizing things like how data dependencies matter, how expensive it is to increase the width of your vector elements (and how to avoid the need), how to turn conditions and branches into masks, or simply things like "division does not exist" becomes a lot easier when you have spent at least some time trying to use SIMD yourself.
> <i>what can be fast</i><p>I think this doesn't get talked about enough. If your input is a big run of data that is being checked/transformed in one shot, it works well. But, if you're likely to have to make a decision on several bytes of the input, SIMD will be the same or slower than the scalar method. It's not a magic "go fast" button.
That is not necessarily true. simdjson exists. But it's far from simple.<p>I have been told SIMD is good for data-parallel loops, like the GPU is, and that is true, but it can also be used piecemeal, unlike the GPU. Because it is just the CPU, you can read 32 unaligned bytes, scan for the index of the first space, and take a branch based on that.
> That is not necessarily true. simdjson exists.<p>I am speaking exactly of simdjson. If you look closely, you'll see that the high performance is really only achieved by <i>skipping over</i> large chunks of the input data. Which is great, if your use case is operating on a subset of data. But, if the application really needs to process every piece of the entire input, there's no performance win.<p>Think about it, JSON structure is almost entirely made up of single bytes ({ } [ ] , : "). If you're not skipping over stuff, you're not going to get away from having to examine those bytes and branch on them. For something like `{"a":1,"b":2}`, the SIMD scan has a bunch of overhead to find the interesting parts and then you go back and practically have to reprocess every single byte.
I think the big miss is that the former is achievable far more often than people imagine, which leads to settling for the later, aka pessimization. Reducing your allocations from millions per run to handfuls per run during initialization is effectively a "go fast" button, and is generally more reliable. It is very common for teams to spend big effort getting a 2-3x speedup on allocation-heavy code by disabling branches when a 100-1000x speedup can be had if restructuring allocations is a strategy under consideration (even before the extra 4-8x you might see if you go all the way to hand-tuned SIMD).
> <i>Every developer should know at least that much SIMD.</i><p>> <i>This [...] applies to any programming language. Support for SIMD instructions varies by programming language</i><p>This is a very pedantic nitpick because this article is good (& getting SIMD support across more languages would also be good), but the "every programmer should know" line feels a bit odd when neither of the 2 most popular languages natively support SIMD.
I started learning multi-platform (x86 + ARM) SIMD last year by writing an audio synthesizer:<p><a href="https://github.com/seclorum/SIMDSynth" rel="nofollow">https://github.com/seclorum/SIMDSynth</a><p>It has been a very rewarding experience, and the synth architecture - multitimbral polyphonic - provides a great stream of data for applying SIMD principles, i.e. multiple streams going through the same process.<p>Has been pretty hard to debug, though. I found myself wishing I had some sort of simulator to help me understand the state of things in each pipe. I suppose I should spend some time investigating SIMD tools next time I get into this - but I fear it'll require a lot more investment. If anyone has any tips, I'm all ears ..
I recommend starting with SWAR [1] before SIMD. Our registers are typically 64 bits, and one can try out SIMD patterns without taking a dependency on particular hardware.<p>This will only be effective if the data you’re working on is smaller than 64 bits. If you’re working with bytes, for example, you might get 8x parallelism.<p>[1] <a href="https://en.wikipedia.org/wiki/SWAR" rel="nofollow">https://en.wikipedia.org/wiki/SWAR</a>
Here's a helpful video about leveraging SIMD to solve a concrete performance problem for the dev team that made the game The Witness by Casey Muratori: <a href="https://www.youtube.com/watch?v=Ge3aKEmZcqY" rel="nofollow">https://www.youtube.com/watch?v=Ge3aKEmZcqY</a>
It's a great talk, I just wish there was a good focused textual version of it, as it is a very long video to recommend to others. Very worth it, but a big investment.<p>It's a great example of what I think of as vertical integration for performance. As you go through the talk you can understand why all these abstractions exist and why they have to be so generic. But when you have a specific use case, you can vertically integrate from the problem definition all the way down to SIMD and reap big rewards.
I like SIMD, but before super-optimizing your code with SIMD and the like, really consider your data structures and access patterns.<p>I've been singing Data-Oriented Design's praises, so I'll just collect all my comments here [1], but I think it's a good approach to optimization. I played around with SIMD in my old code (in Zig), but my approach to modelling datastructures was so antithetical to optimization, it was like putting high-performance racing tires on a lemon with a broken engine.<p>It was the <i>root-of-all-evil-type-premature-optimization</i>, because I wasn't measuring performance, and I wasn't thinking about where the allocations were, etc. Now, I try to model my data as if it were SQL tables, see what my potential "primary keys" could be, and build my data structures around my access patterns.<p>For example, I used to model trees as structs pointing to other structs on the heap:<p><pre><code> struct Tree {
tag: TreeTag,
children: Vec<&Tree>
}
</code></pre>
Now my tree has all the bad characteristics of a linked list (* <i>n</i> nodes * <i>m</i> children), all the fragmentation of multiple heap vectors (* <i>n</i> nodes), and terrible set-up / tear-down time (in this case, Drop alone was taking up a good chunk of runtime).<p>But a tree can be represented a million ways, and can always be linearized. So now I really consider my access/insert patterns of the tree, whether it's really a tree or some other sort of graph, whether I can store it in a Vec or a Struct of Vecs, etc. Since really looking at things through their access patterns and "primary keys", my code has been much faster and simpler.<p>This has the added effect that a lot of your data ends up in homogeneous arrays / vecs, which means that the compiler can do its SIMD magic, the CPU can read it from your L1 cache a million times faster, etc. And then when you need to drop down into SIMD yourself, you can write some awesome branchless code.<p>1. <a href="https://hn.algolia.com/?dateRange=all&page=0&prefix=true&query=Data-Oriented%20Design%20author%3ARendello&sort=byPopularity&type=all" rel="nofollow">https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que...</a>
Yeah, data layout/cache aware layouts are really key if you really want to unlock making something that ends up in a hot loop fast with SIMD.<p>Also, avoiding allocations or vtable lookups or a lot of indirection in the part of the code that's actually "hot" is really important. Vectors (in C++) at least aren't necessarily the best fit either, if you end up doing anything that can call an allocation unexpectedly.
> Vectors (in C++) at least aren't necessarily the best fit either<p>I'm not sure if you use a different allocation strategy or if you're advocating allocating as much as possible up-front, but I'm curious if you have any thoughts on this:<p>I always end up using (Rust) vectors despite looking at a bunch of slab/arena allocation libraries. Preferably I'd know how much memory I need up front, but barring that I see three options for any allocation that needs to grow:<p>- Fail;<p>- Reallocate; or<p>- Put the overflow in a new allocation, keeping track of where all the "pages" are internally.<p>In 2, you can't use references/slices (anything with a pointer) because the potential reallocation invalidates those. In 3, it seems ideal because references can stay stable, but you wouldn't be able to have any array-like data cross the "page" barrier as the pointer jump would not be stable. (Although, am I correct in thinking the OS does something like this, and that's why pointer addresses are virtual?).<p>So, you can't really internally reference data in this sort of context by reference/pointer, it's preferable to use an integer index. In that case, what's the point of the allocator libraries at all? Your standard Vector would have the same reallocation/access characteristics, and you can set a reasonable initial capacity to try and avoid reallocations.
To your virtual memory comment, the kernel can only do as it's told, but allocators can indeed tell it to shuffle pages around in virtual memory. On Linux that's `mremap` [0], and `realloc` implementations [1] use it for large enough Vecs (apparently 128kiB for glibc and musl). macOS' libmalloc will try to map new pages that extend large allocations in-place, but memcpy elsewhere if existing virtual mappings are in the way. I don't think Windows' HeapReAlloc does either, but they might reserve a larger virtual region and incrementally commit it or something to similar effect.<p>[0] <a href="https://man7.org/linux/man-pages/man2/mremap.2.html" rel="nofollow">https://man7.org/linux/man-pages/man2/mremap.2.html</a><p>[1] <a href="https://git.musl-libc.org/cgit/musl/tree/src/malloc/mallocng/realloc.c?id=b306b16#n34" rel="nofollow">https://git.musl-libc.org/cgit/musl/tree/src/malloc/mallocng...</a>
A fourth alternative, in 64-bit systems, is to reserve a stupidly large chunk of memory up front with `mmap()` or equivalent (`malloc()` actually should work about as well). That way you guarantee that any extension will happen in place. There’s a limit to how much you can reserve, but since that limit is much higher than what you can actually use, you can make quite a few of those reservation before you run out of address space.<p>It feels dirty, but when your system has overcommit you can’t reliably check that the memory you want is actually there anyway, so you might as well reap the benefits.
In Rust at least, most things have a with_capacity(<i>n</i>) constructor to ensure there's space for <i>n</i> elements (or <i>n</i> bytes, in the case of strings). I suppose there's no getting around the fact that if your collection has no known bounds, you'll have to do bounds checking + potential reallocation in the hot (push) path or risk having your program SIGSEGV.<p><a href="https://doc.rust-lang.org/std/?search=with_capacity" rel="nofollow">https://doc.rust-lang.org/std/?search=with_capacity</a>
In 32-bit systems too, but the limits are lower.<p>Windows is cleaner, as it lets you reserve address space and then later commit it. The Linux equivalent to reserve is probably mapping with no permissions. Overcommit can be disabled on Linux and doesn't break control flow integrity or valgrind, so I know there is a way.
The main reason for virtual addresses/TLB is to prevent processes from accessing each other's memory (isolation). For optimization I'd say the page size (64 kB) is a secondary concern. It's handled in hardware (TLB) with the OS only occasionally filling up the mapping. You want to avoid that happening, but you probably should worry more using whole cache lines (64 bytes) instead, and beyond that just keep memory access local (multiple cache hierarchies) and predictable (pre-fetcher).
This is my eternal battle as a perf engineer. Performance starts with architecture and you can only squeeze so much out a hotpath with poor data layout.<p>The nice part is that data-oriented code almost always easily supports threading and SIMD.
Very much agreed. Even more basic than that - memory access patterns are important. The amusing thing is that you end up writing GPU-style code even for CPU. For example - instead of an array of objects, using parquet-style object of arrays is one such trick.
There's been some good implicit/explicit discussion about SoA on this post [1]. For anyone curious, there are a few different terms that refer to basically the same thing:<p>- Struct of Arrays (SoA) vs Array of Structs (AoS);<p>- Row-major order vs column-major order; and<p>- Row-based vs column based / columnar (in databases)<p>Most code has arrays of structs (or "lists of objects", the effect is the same), and most databases are row based (same thing). But many game engines use SoA and keep heterogeneous elements together. Some databases like DuckDB do this too, this is an article about the pros and cons of columnar storage in DBs [2].<p>1. <a href="https://news.ycombinator.com/item?id=49012056">https://news.ycombinator.com/item?id=49012056</a><p>2. <a href="https://motherduck.com/learn/columnar-storage-guide/" rel="nofollow">https://motherduck.com/learn/columnar-storage-guide/</a>
This. Tables are an efficient implementation of general graphs. It’s the best one I know of (unless your graph can be specialized).
> Every developer should… most importantly, not be scared of SIMD<p>Seems like he should be recommending fearless_simd [1], the Rust crate by Raph Levian and the folks at Linebender :)<p>More seriously, if you’re looking to add SIMD to your Rust code, that’s the package to start with.<p>[1] <a href="https://crates.io/crates/fearless_simd" rel="nofollow">https://crates.io/crates/fearless_simd</a>
Tangentially for Go programming, the last time I looked at optimising some Go code with SIMD there were a few different options available, but they were either not maintained any more or had incomplete support and required first writing your function in C++ with intrinsics and generating assembly, then converting it to go assembly with a tool [1]. I never got my function to work in go despite the C++ code working fine. In short, not really a production ready option for Go. This was a year or two ago, though.<p>Edit, there's now an experimental official library at <a href="https://go.dev/pkg/simd/archsimd/" rel="nofollow">https://go.dev/pkg/simd/archsimd/</a> see <a href="https://go.dev/doc/go1.26#simd" rel="nofollow">https://go.dev/doc/go1.26#simd</a> and at <a href="https://github.com/golang/go/issues/78902" rel="nofollow">https://github.com/golang/go/issues/78902</a> so things have moved since I tried it last.<p>[1] <a href="https://github.com/minio/c2goasm" rel="nofollow">https://github.com/minio/c2goasm</a>
And here I am pooping out simple typescript... These articles always make me feel like I'm wasting my talents working on products that don't really have the need to leverage any understanding of what's happening under the hood at the CPU instructions level.
we make extensive use of simd at work, usually through highway: <a href="https://github.com/google/highway" rel="nofollow">https://github.com/google/highway</a><p>imo this is one of the greatest libs ever written. it handles dynamic dispatching of correct simd instructions / lane widths for various hardware with just one simd loop written (handling NEON/AVX/AVX2/AVX512/extensions) with comparable performance to handwritten native intrinsics
The Go language has long lacked official support for SIMD instructions, which means it has been at a disadvantage in terms of performance optimization. In recent years, with Go 1.26, an experimental version of the SIMD/ArchSIMD packages was introduced for AMD64 architecture. With Go 1.27, a portable version of the SIMD package was also added. Now, we can fully utilize native SIMD instructions to optimize go program performance.<p>- <a href="https://pkg.go.dev/simd/archsimd@go1.26.5" rel="nofollow">https://pkg.go.dev/simd/archsimd@go1.26.5</a>
I think too many people get dismissive of reaching out to Assembly in other languages, while in C and C++, having to reach out to Assembly to do exactly the same is seen as an advantage versus other languages.<p><a href="https://github.com/kelindar/simd" rel="nofollow">https://github.com/kelindar/simd</a><p><a href="https://github.com/viant/vec" rel="nofollow">https://github.com/viant/vec</a><p>However, having it officially supported is definitely much more convenient.
i was having a conversation with a friend recently about simd in zig (which i have recently picked up and been having a pretty good time with). i find that simd writes decently well, though there's a few weird things:<p>- some builtins purport to work on simd vectors but actually just unpack the vectors and do their work per-element (e.g. running `@sin()` on a `@Vector(4, f32)` will unpack the vector, run `@sin()` 4 times, and then pack it back into a vector).<p>- a lot of `std.math` is scalar-only (some functions support vectors, though, and i've got a pr open for one of them and plan to do more).<p>- i'm certainly missing some intrinsics that i get from xmmintrin.h (rcp, rsqrt, few others).<p>in general though i'm finding it pretty capable.<p>mitchell, i know you hang around some of these comments sometimes – i noticed that in ghostty you bring in some c++ libs to do the simd heavy lifting for you. any plans to port that to zig? anything missing from the language or libs that's preventing it?
> mitchell, i know you hang around some of these comments sometimes<p>hi im here<p>> i noticed that in ghostty you bring in some c++ libs to do the simd heavy lifting for you. any plans to port that to zig? anything missing from the language or libs that's preventing it?<p>No plans to port it. For others, this is referencing highway: <a href="https://github.com/google/highway" rel="nofollow">https://github.com/google/highway</a><p>The major limitation of Zig's vectors is that they're compile-time only. So if you're building redistributed software that compiles for a baseline CPU target, it won't be as optimized as it could be for YOUR possible machine.<p>Highway compiles our SIMD modules for different hardware configurations and at startup does a CPUID fingerprint to figure out which to load. That way even baseline has AVX512 etc. implementations, and we just activate the right one at runtime.<p>We only use Highway for our hottest hot paths that we feel benefit from that specialization.<p>No plans to port that (although, I spent hundreds of dollars and slop-forked it into Zig with the help of this good boy GPT and it worked great actually, but I didn't want to maintain it).
ahaaa, yeah, i don't personally do any runtime switching but i hear that as a deal-breaker from other folks.<p>it's interesting – i've found that zig tends to extend my vectors to the native width of the platform and then operate on them there. e.g. i had a `@Vector(2, f32)` that i was using as a demo and the generated assembly was promoting it to 256 bits and using avx2 instructions on it!
Why does your website block Tor so I can't read the article?
> some builtins purport to work on simd vectors but actually just unpack the vectors and do their work per-element (e.g. running `@sin()` on a `@Vector(4, f32)` will unpack the vector, run `@sin()` 4 times, and then pack it back into a vector).<p>this is reasonable because there isn't really a generalizable "good way" to unroll trig functions for simd. if you really care about speed youre better off implementing to the precision you care about (you might not want full precision)
The loop dependency on a poly isn't all that hard for compilers to unroll, and most correctly rounded implementations are polys. You're often paying only a couple cycles' worth of stalls.<p>We can see this in action. LLVM implements sin() with a correctly rounded double poly [0]. Let's ignore range reduction and throw the core into compiler explorer to be autovectorized [1]. uiCA estimates a theoretical latency for the inner loop of 15 cycles, and the code achieves 18.<p>I suspect most custom implementations would do worse than this.<p>[0] <a href="https://github.com/llvm/llvm-project/blob/165c472d65cd62eb332246eb39901603e4b1e4ce/libc/src/__support/math/sinf.h#L147" rel="nofollow">https://github.com/llvm/llvm-project/blob/165c472d65cd62eb33...</a><p>[1] <a href="https://godbolt.org/z/rGGq8aG38" rel="nofollow">https://godbolt.org/z/rGGq8aG38</a>
i don't disagree – i have my own internal vector lib of approximations and whatnot for various tradeoffs of precision and speed, so i just use those. it's just that zig has a pretty strong stance of "no unexpected/obscured code execution" so it was surprising to see a vector-capable function that was just a bunch of scalar functions in a trench coat.<p>maybe functions that don't actually support actual vector execution just shouldn't work on vector arguments. i also wouldn't expect `@sin()` to expand in-place out to a full cephes-like sin implementation. maybe a function call.
Why isn't there? It's usually just a bunch of math and short branching that can be replaced with masks.
I bet there's a better way than unpacking, running sequentially and repacking. Even if the algorithm is very branchy you save a pack and unpack.
<a href="https://sourceware.org/glibc/wiki/libmvec" rel="nofollow">https://sourceware.org/glibc/wiki/libmvec</a> ?
"More importantly, when this loop matters enough for me to care about a 5x speedup, I want the vectorization to be explicit and predictable. I don't want an unrelated code change or compiler update to quietly turn it back into a scalar loop."<p>Only tangentially related but this is by far the most painful part about optimizing code for JIT compilers like V8. Even changing a constant from 1 to 1.0 somewhere else can change the optimizations performed and lead to an unexpected performance decrease.
I've been using the Vector API in Java to get some massive speedups for flowfield generation. There's no guessing with that approach - if the hardware supports SIMD, you get it.
Now with Valhala finally getting merged, he can hope the end of preview releases for the Vector API is coming to an end, probably it will take at least until Java 28, though.
Does anyone know of a good hands-on introductory and practical tutorial on SIMD? I know that SIMD doesn't imply particular instruction set but a pattern of concurrent data transformation.<p>I guess what I'm looking for is something that addresses the common idioms in SIMD. For example, want to do concurrent data look up? This is how you do it in SIMD; this is how you search; this is how to prepare your data in a manner conducive to SIMD operations; these are the data types you typically find in programming languages, like __mm128; so on and so forth.
> I know that SIMD doesn't imply particular instruction set but a pattern of concurrent data transformation.<p>Eh. It's both. You don't have one without the other. SIMD is pretty much entirely extensions to base processor ISA; there might be a few architectures that have SIMD as a basic part of them (GPUs are one of them, taken to an extreme order), but for the most part you have to know which instruction set you're working with.<p>SIMD is basically "pack multiple data points into a single CPU register, then to another, and perform some operation on them as if you ran that op on all of the pairwise data points individually". Some ops are binary (arithmetic, bitwise, etc) and some are unary.<p>Some have "gates" whereby you can do a comparison, the boolean output of which is stored in a bit packed integer. Then you can run conditional instructions after that that only perform the instruction if the bit in that variable is set, creating "constant time" SIMD instructions with what amounts to branching. Really depends on the instruction set's capabilities.<p>AVX512 is far and away one of the most extensive extensions, with a ton of super niche instructions meant for enterprise number crunching. It has weird stuff, like swapping bytes, collating them, doing all sorts of weird manipulations. But the number of x86 CPUs that support that instruction set are small.<p>You can't emit code that has e.g. AVX512 and just run it on a CPU that doesn't have that extension. You get a CPU exception and it crashes the process (or, if this is in kernel/driver land, your machine).<p>So they're kind of tied together.<p>Anyway if you want to see a list of them, Intel's SIMD intrinsics site has always been really nice to browse IMO.<p><a href="https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html" rel="nofollow">https://www.intel.com/content/www/us/en/docs/intrinsics-guid...</a>
>> you'll begin to naturally decompose every for loop into these five steps<p>Does zig have auto vectorisation? I'm thinking that if you write the code in a vector friendly way, then the compiler can do the boiler plate for you<p><a href="https://llvm.org/docs/Vectorizers.html" rel="nofollow">https://llvm.org/docs/Vectorizers.html</a><p><a href="https://inside.java/2025/08/16/jvmls-hotspot-auto-vectorization/" rel="nofollow">https://inside.java/2025/08/16/jvmls-hotspot-auto-vectorizat...</a>
The example code in this article is using Zig's portable SIMD features. Similar features are available for C/C++ (GCC/Clang extension) [0], (nightly) Rust [1] and C++26 [2].<p>All of these provide a similar set of features and you can use normal arithmetic operations (+, -, *, etc) for SIMD vectors. Together with templates/generics you can also write code that can deal with any vector width. These get compiled to LLVM vector types and will generally give you pretty good generated code.<p>This is a very good way of writing basic SIMD code and has the benefit that your code can be compiled to multiple instruction sets. I've been working on a project that can compile down to SSE2, AVX2, AVX-512 and NEON, with just a change of compiler options. Somewhat surprisingly I get the best performance by using 2x the native vector width (ie. f32x16 = 512 bits on 256 bit AVX2), which is kinda like unrolling the loop once.<p>There are some caveats, though. You will need to keep an eye on the generated assembly code to make sure you're on the happy path. You will inevitably need to drop down to ISA specific intrinsics every now and then (for that fast reciprocal square root with `__mm_rsqrt_ps` etc).<p>As an example I needed to do a gather load from an array of fp16's on AVX2, which does not do 16 bit loads. Rust's `Simd::gather_select` takes 64 bit usize as the index but AVX2 doesn't do 64 bit indices. But as long as I did all the index arithmetic in 32 bits and cast to usize at the last second, the compiler did what I wanted. But you need to kinda know what is available in the ISA to stay on the happy path. Not really an issue with arithmetic.<p>I'm sure that an experienced SIMD programmer can get better performance by writing intrinsics manually (say 5-20% better) but I'm already at 3-6x better than the scalar implementation I started with. And you'd have to write (and benchmark) the code for each ISA separately, meaning that you'd spend at least five times more time with it (and have 5x more code to maintain).<p>[0] <a href="https://gcc.gnu.org/onlinedocs/gcc-4.6.1/gcc/Vector-Extensions.html" rel="nofollow">https://gcc.gnu.org/onlinedocs/gcc-4.6.1/gcc/Vector-Extensio...</a>
[1] <a href="https://doc.rust-lang.org/nightly/std/simd/index.html" rel="nofollow">https://doc.rust-lang.org/nightly/std/simd/index.html</a>
[2] <a href="https://en.cppreference.com/cpp/numeric/simd" rel="nofollow">https://en.cppreference.com/cpp/numeric/simd</a>
FYI you linked to a really old version of the GCC documentation. Google apparently loves those old docs, so they often show up near the top of search results despite being ancient. For posterity, here's the latest version: <a href="https://gcc.gnu.org/onlinedocs/gcc-16.1.0/gcc/Vector-Extensions.html" rel="nofollow">https://gcc.gnu.org/onlinedocs/gcc-16.1.0/gcc/Vector-Extensi...</a>.
This is an interesting article. I don't really work with low level enough languages for this to matter (unless - does this ever show up in Javascript somehow?).<p>I guess I don't understand the "reduce" step. It seems like you have to be careful not to "undo" all the benefit from SIMD. Sure, it can compare 8 values in parallel, but then if you have to look at each of the 8 answers in turn you're back to where you began. Is the `@reduce()` function in the example a special Vector one that tells you if all the values are true or not in one "step"?
> Is the `@reduce()` function in the example a special...<p>Yes, it is. In SIMD you can do a reduce for a commutative function in O(log N) steps. Sum, product, min, max, all, any, etc.<p>Although in this case it might be better if you just take the mask (vector of booleans), convert it to a bitmask and check if it is (non-) zero.<p>If the language provides it, you might also use .all() or .any() for a mask.
Yes. "`@reduce(.And, ...)` combines every boolean using `and` and returns a single boolean."<p>If it's true (the common case here) then you proceed to look at the next 8 bytes.<p>If it's false, you apply a @bitcast (turn the booleans into bits) and @ctz (find the first 0) to get the index of where it was false.
It can show up in JavaScript if you write your code in a way that lets the browser engine lower it to SIMD under the hood
The vast majority of developers have 0 need for learning SIMD. Why mislead them, and make them feel like to be a "real" developer they have to know it?
It distresses me that we don’t have a language that can do a best effort parallelization of arbitrary loop like code across SIMD, multiple threads, multiple cores and GPU with a small directive.<p>I don’t need it to be optimal, just … handy as an option!<p>The last time I brought this up here, folks offered a bunch of options that don’t quite do this, and the best candidate was this 15 year old compiler project that is Intel specific!<p><a href="https://ispc.github.io/" rel="nofollow">https://ispc.github.io/</a><p>Could some programming language nerd build this?<p>(While you are at it give me a clear idiomatic way to pay the cost to switch from array of structs to struct of arrays)
The problem is you need both a PL nerd and a performance nerd and while that group has some overlap so these people are not as uncommon as you’d think the task is pretty hard so you need a lot of people on it, with a bunch of funding, etc. Usually it’s just cheaper to rewrite all your code by that point and so these efforts fail
> <i>best effort parallelization of arbitrary loop like code across SIMD, multiple threads, multiple cores and GPU with a small directive.</i><p>I doubt GPU is included by most runtimes yet, but for the rest of that have you tried SQL?
Good answer, too many folks miss out how powerful SQL actually is, and with stored procedures its compilation to native code can even cached across executions.
A genuinely funny and good point!
It's computer vision focused and might have been suggested previously, but I think Halide is a pretty good/mature demonstration of one way to approach this - writing the algorithm and the execution descriptions as separate passes with access to auto-optimisers and GPU runtimes.
Chapel aims to do that, however they only focus on HPC as userbase.
ISPC isn’t Intel-only: <a href="https://github.com/ispc/ispc" rel="nofollow">https://github.com/ispc/ispc</a>
> parallelization of arbitrary loop like code across SIMD, multiple threads, multiple cores and GPU<p>The reason we don’t have this is that it’s a holy grail, an unsolved problem, for quite fundamental reasons.<p>The other comment about SQL hints at why: SQL is largely declarative and has complex semantics built into the language, which allows for analysis and optimization that go beyond what’s possible for lower-level, general purpose languages, especially imperative ones.<p>For code in those languages, even just determining whether “arbitrary loop like code” is parallelizable is undecidable in general.<p>The challenge is that as you make a language expressive enough to describe arbitrary algorithms, you also make it progressively harder for a compiler to infer safe and useful parallel execution automatically.<p>Another big issue is that the various forms of parallelism are only similar at a very high level. They have fundamentally different execution models and constraints. Translating arbitrary imperative code to handle that essentially involves first inferring the intent of the code, then rewriting the code, including how data structures are organized, to fit the target architecture. This is far more than what ordinary compilers do.<p>There are also a lot of choices involved. Parallelism isn’t always free, so you’d need to make sure that the costs don’t outweigh the benefits - and you’d need to do that for many different decisions, like whether to use threads or not. Now you’d have a compiler building cost models to try to not make dumb choices - and without actually restarting and comparing alternatives, it’ll make mistakes.<p>In many ways, you’d be better off using an LLM for this, because that’s the level of understanding you need to have a hope of getting a good result.<p>That all said, you can do much better with more constrained languages or frameworks. SQL is the most successful example of that. Java’s streams and Rust’s Rayon only target multicore CPUs, but similar approaches could be used to do more. (Although you still potentially run into issues with optimal data shape across paradigms.) Languages like APL, J, and Futhark are all relevant.<p>The other family of solutions to this are the frameworks like Apache Spark, Apache Beam, and the ML frameworks like Pytorch. The latter lets you describe (tensor) computations at a high level, leaving the framework free to figure out how to implement them - much like with SQL.
>It distresses me that we don’t have a language that can do a best effort parallelization of arbitrary loop like code across SIMD, multiple threads, multiple cores and GPU with a small directive.<p>It's called ParaSail. The next closest thing to ParaSail is ...<p>... literally just Rust.<p>Why? Because ParaSail has completely eliminated pointers, thereby preventing pointer aliasing. It can't be understated that pointer aliasing and alignment are the two biggest bottlenecks preventing autovectorization.<p>The people talking about better compilers, etc, just don't get it. It's not a compiler problem, it's a language semantics problem.<p>Pointer aliasing prevents parallelism full stop. If there is a single memory region and you perform a write to it, you have to assume that the write invalidates all data loaded from the pointers. If you make pointer aliasing illegal, then you have guaranteed that each pointer points to a distinct subset of the global memory, turning each pointer into a pointer to an isolated region. This means you have multiple regions you can write to in parallel. This is crucial, if you do not understand this you don't get parallel programming at all.<p>I mean think about it, this is the difference between having a four toilet bathroom with a single door or four doors.<p>The thing about alignment is not as easy to explain, but here is my attempt at it: If you allow the array to be misaligned at the front, then you have to run scalar code at the front. Same problem if you misalign at the end.<p>If you have a naked pointer and a for loop (think C), then the alignment problem alone precludes autovectorization without a complex scalar preamble. Autovectorization has to assume that the loop length could be anything, meaning it could be less than 8 elements to begin with. If the loop is always a multiple of 8 and always aligned, then the loop can be autovectorized even if the loop only does a single iteration.<p>Of course, after these critical blockages are gone you're still stuck with the problem of having to write branchless/non-diverging code.
Remember that bug with Intel Skylakes [0]?
When an application used AVX, it slowed down everything else on that node. It was by far not easy to debug why some applications randomly suffered perf hits on a new hardware being rolled out in Azure.<p>[0] <a href="https://arxiv.org/abs/1901.04982?utm_source=chatgpt.com" rel="nofollow">https://arxiv.org/abs/1901.04982?utm_source=chatgpt.com</a>
It was an early AVX-512 unit that resulted in down lock when you used more than a few instructions in short time, as well as resulted in a Linus rant.<p>Later CPUs (updated skylake xeons and later, AMD Zen 4 and newer) don't have the issue
Historically, it was a bit more complex than that, and applies to more than just AVX-512: <a href="https://gist.github.com/rygorous/32bc3ea8301dba09358fd2c64e02d774" rel="nofollow">https://gist.github.com/rygorous/32bc3ea8301dba09358fd2c64e0...</a>.
The Intel server CPUs Skylake Server, Cascade Lake and Cooper Lake, which had bad frequency/voltage management are now ancient history and very few of them have been used as workstation CPUs by individual users.<p>AMD Zen 4 and Zen 5, and also those Intel CPUs with AVX-512 support starting with Ice Lake, behave much better and there is no reason to avoid AVX-512, which has much better energy efficiency than the alternatives.
I enjoyed it. Great is the fact that examples are given.
Most of my stuff isn't CPU-bound. Most of it is in managed languages, like Bash and C# and SQL and Python and Yaml and .tsx .<p>It's been at least a decade for me since SIMD was more than an implementation detail handled by the runtime. And even then it was "how do I avoid preventing the runtime from vectorizing this".
Everyone doesn't need to know SIMD. Mechanical sympathy is an important passive perk for software architects to cut down the number of reworks down the line, but I would rate benchmarking and being able to identify bottlenecks as more important everyday skills.<p>I'm working on a voxel space renderer homebrew for the PlayStation. I only have so many cycles to spend on rendering before it becomes a slideshow, so I count them in my hot rendering loop and parallelize work as much stuff as I can, even across memory load stalls from main RAM.<p>I've worked on a basic network accessory card with a STM32 MCU that is extremely overkill for what it needs to do. We haven't bothered making any performance or memory optimizations whatsoever, writing plain C++ almost as if we were on server-class hardware because we had such egregious margins.<p>The first question to ask is not whether something can leverage SIMD, it's whether the performance requirements are met or not (although it's far too easy to not care when it's not your hardware that's struggling...).
> Mechanical sympathy is an important passive perk for software architects to cut down the number of reworks down the line<p>We might be talking on different levels but when it comes to, on an opposite end of 'Should I use SIMD'... a databases a level of mechanical sympathy at a 'base' level is still important. e.x. row-by-row updates vs batching or bad logic where a 21k entry in clause forgot about unicode rules on columns and breaks an index [0]... is still super important.<p>[0] - That one is real, thanks lazy bodyshop having their people use copilot and yet, we get the same billable hours, nothing is done faster, and management is too stupid to pay attention...
To do some optimization work with SIMD what you actually need to understand is the underlying CPU uarchitecture, intrinsics by the end of the day are just an API. But to also make sense of the benchmarking results or bottleneck debugging you also need to understand the underlying CPU uarchitecture and/or further devices your workload might be utilizing, e.g. storage.
> ....it's whether the performance requirements are met or not ...<p>This is what so many people miss when doing micro-benchmarks of language X vs Y, sure Y might win out in execution speed, however if X delivers within the performance requirements and has a lower development cost, it wins out while being slower than Y.<p>Naturally taken to the extreme, when it isn't our hardware is how we end up with Electron apps.
It’s worth knowing SIMD for the purpose of knowing when it’s not worth using
I agree in spirit, but most serious cases of this class of problem have moved to accelerated kernels (e.g. GPU). Which has some commonality but is different enough that a lot of these learnings don't translate.<p>There might be a narrow class of problem where:<p><pre><code> - SISD is the bottleneck
- Compiler won't autovectorize
- Data is small or weird enough, or the environment constrained enough that running it on an accelerator is not feasible
</code></pre>
But that seems an increasingly small scope for something "everyone should know".
The biggest barrier I faced learning SIMD is the weird naming convention for intrinsics.<p>Once I got past that, it was fairly simple.
mcyoung's articles were also super helpful
One of my favourite articles is "SIMD-friendly algorithms for substring searching" by Wojciech Muła [2]. If you were unfamiliar with SIMD and just jumped into the code, it'd be incomprehensible due to the intrinsics, but the generic algorithm description at the top is pretty simple if you take some time understand it.<p>It blew my mind once I understood what was happening, because it's quite clever but one of those "I could've thought of that" algorithms. There was some pretty good discussion on it last year (feat. ripgrep). [2]<p>1. <a href="http://0x80.pl/notesen/2016-11-28-simd-strfind.html" rel="nofollow">http://0x80.pl/notesen/2016-11-28-simd-strfind.html</a><p>2. <a href="https://news.ycombinator.com/item?id=44274001">https://news.ycombinator.com/item?id=44274001</a>
Isn't the better abstraction here to use a higher level library in the style of pandas/polars that will operate as vectors, compose and feel readable and inuitive, while (almost?) maxing out SIMD?
If you’re processing N things at a time and your “scalar tail” is N-1 why can’t you put in a dummy value for the last entry, run one last SIMD iteration, and discard the dummy return value?
You can, but usually the code may have problems with this. For example, storing out of bounds is often going to give you a bad time. Some platforms that are all SIMD all the time will support masked operations for this kind of thing.
I don’t know zig syntax, but wouldn’t it be possible to put this common pattern into a macro and simplify it to mostly a lambda on V?
Is SIMD in rust still pretty bad?
Is interesting that this is how array langs work.<p>I bet will be easy to turn into a lib.
I always must ask, why isn’t your compiler doing this for you? I know they often aren’t because I’ve seen speed ups from writing SIMD or using the vector functions in MKL, but this is something I really think the compilers should do for us in the simple case.
Love Mitchell’s writing and he’s one of the few people in the industry that I truly admire.<p>But you need a better color scheme for your light theme my guy. Greys on greys on whites with bright pinks and light blues…it’s really hard to just read.
It seems like somewhere in there he could have explained the acronym.
99% of developers should just ignore SIMD. Most projects have a lot of low hanging fruit to increase performance, and still nobody finds the time to solve them.
My compiler knows SIMD. However, knowing the limitations of SIMD might help avoid a calculation that can't be optimized to use it.
Code should at all points be easy to reason about. Is SIMD easy to reason about? No.
The author of this post seems to be unaware that many popular programming languages don't even support writing explicit SIMD instructions, which undermines its main thesis.
I think more developers would use SIMD if there were macros handling the details.
Easier than ever, with RISC-V Vector (RVV), which is part of RVA23.
Or... You know... Tell your favorite agent to "SIMD this" (:
Not really, not as SIMD remains an esoteric art from packing matrix and vector operations in endless opcodes.<p>I rather let the compiler auto vectorise itself, or with AI help.
SIMD does not pay, speaking from 20yr exp in the field in various semis.
I was hand-rolling NEON SIMD 15 years ago, and in many cases the compiler (clang/llvm) simply out optimized me. I kept the attempts that were better than what the compiler could already do. That was ARM NEON, quite new at the time, not SSE, and again that was 15 years ago that the compiler could already beat me much of the time. I hate the naive cult coder adage concerning premature optimization, but this might be a situation where you peruse your compiler output before you start writing code in a manner the compiler can for you. In Clang, auto-vectorization is enabled by default at optimization levels -O2 and -O3.
[flagged]
"<i>Please don't sneer, including at the rest of the community.</i>" It's reliably a marker of bad comments and worse threads.<p><a href="https://news.ycombinator.com/newsguidelines.html">https://news.ycombinator.com/newsguidelines.html</a>
s/site/industry/g
No kidding. I saw no less than five "the compiler will do everything for me, I trust in the magic" comments before I gave up reading through the thread.
[flagged]
[dead]
[flagged]
[dead]
[flagged]
I just do gcc -O3 and get SIMD without having to learn it
In the article, Mitchel mentions how this doesn’t always work. In fact, as someone who’s worked in compiler development, I can say it’s a small miracle when it <i>does</i> work.
Case-in-point, the example in my own post doesn't auto-vectorize with LLVM or GCC at highest optimization levels. Basically, compilers will never auto-vectorize loops with an early loop break afaik.
You need to let the compiler know that there are at least 4 or 8 elements to process. This may require padding data and/or having a second loop after the main one that processes the remainder <4 or <8 elements.<p>You start the post with:<p>> There is an opportunity to use SIMD. SIMD turns those into this:
>
> for (8 byte chunk in bytes) { /* ... */ }<p>If you actually wrote that loop, there is a good chance the compiler (gcc specifically) will auto-vectorize.<p>In any case, the more manual SIMD optimizations I have seen require reworking the data altogether, not just processing N elements at a time. For example, instead of packing two 4-vectors into two registers to do a dot product, pack the XXXXs, YYYYs, etc. into 4 vectors and compute 4 dot products for the price of one. That not only requires having 4 vectors to process, but also thinking how exactly they are packed in registers.<p>I don't know why qurren is downvoted. You really should see if you can get the compiler to auto-vectorize first (possibly padding data structures and loops) before you write anything by hand.
Most scalar-to-SIMD conversion requires changing the design of data structures and algorithms to be effective. Compilers are required to exactly reproduce the specified data structures in a deterministic way for obvious reasons.<p>Even if compilers were clever enough to transform your data structures and algorithms for SIMD (they're not), the data structures are a contract that can't be unilaterally modified.
auto-vectorization is not nearly as good as you would hope it to be.<p>The best SIMD optimizations likely require changing your data format from AoS to SoA.
> The best SIMD optimizations likely require changing your data format from AoS to SoA.<p>We do have gather load instructions in SIMD instruction sets these days (AVX2 and newer), so AoS vs SoA is not nearly as important as it was once.<p>Scatter stores are also available but only in newer CPUs.
While C++ may be reaching levels of Algol 68, PL/I complexity, with C++26 reflection you can do automatically.<p>See <a href="https://github.com/cern-nextgen/reflmempp" rel="nofollow">https://github.com/cern-nextgen/reflmempp</a>
The one feature in Jonathan Blow's Jai language I really envy is a a single keyword to switch AoS to SoA and visa-versa at comptime
Either this or you have to do special tricks like pairwise tree reductions and hand-unroll certain portions of loops.
What are AoS and SoA?
Array of Structs and Struct of Arrays <a href="https://en.wikipedia.org/wiki/AoS_and_SoA" rel="nofollow">https://en.wikipedia.org/wiki/AoS_and_SoA</a>
A good introduction to SoA (for anyone curious) are the two most famous Data-Oriented Design talks by Mike Acton (game engine dev) [1] and Andrew Kelley (Zig lead dev) [2] respectively.<p>I read a book book about DoD [3] really which confused me at first with all its talk about database table design (in a book about a high-performance C++ game engine?), but when it finally clicked it was amazing. The point is that you want to think hard about your access patterns and what could constitute good "primary keys", then model it accordingly. SoA ends up being useful a lot of the time, because having your data in homogeneous arrays/vectors is great for cache locality and branch elimination. Even without SIMD you can get huge speedups from that, but that's also where your compiler (or you as a programmer) can get incredible SIMD gains.<p>SoA is not a silver bullet as it may not align well with your access patterns, but it can great to add to your toolkit.<p>---<p>Mike Acton: <i>Data-Oriented Design and C++</i>: <a href="https://www.youtube.com/watch?v=rX0ItVEVjHc" rel="nofollow">https://www.youtube.com/watch?v=rX0ItVEVjHc</a><p>Andrew Kelley: <i>A Practical Guide to Applying Data Oriented Design</i>: <a href="https://www.youtube.com/watch?v=IroPQ150F6c" rel="nofollow">https://www.youtube.com/watch?v=IroPQ150F6c</a><p>Richard Fabian: <i>Data-Oriented Design</i>: <a href="https://www.dataorienteddesign.com/dodbook/" rel="nofollow">https://www.dataorienteddesign.com/dodbook/</a>
Array of Structs and Struct of Arrays
And -march=native or at least -march=x86-64-v3 or similar, alternatively identifying relevant functions and manually invoking FMV and uarch specialization via target_clones. Plus non-integer code can generally not be autovectorized in normal-math mode since FP is non-commutative.
Well, then I just prompt Claude and get SIMD without having to learn it /s
I have no idea why you're being downvoted. HN has a fetish for SIMD, but if you are hand-rolling SIMD and you aren't writing an explicit acceleration library, you're doing it wrong. Like, 100% of the time.<p>Every modern language has a vectorization optimizing compiler, and through some fairly straightforward techniques this is automagic. And contrary to the various replies, unless you screwed something up compilers are really good at vectorizing on whatever hardware you're targeting, including SVE.
Everyone should know <i>about</i> SIMD, so you can know when to ask your good friend Al, who knows how to do it, to use it for you in the right places.
I respect (and fear a little) those who intentionally utilize SIMD in their implementations, but I believe it's a bit too much of a semantic shift for 2-5x performance gain. Good news is that modern compilers are more than capable of emitting SIMD code even if original source is nothing but.<p>Most software's poor performance would be fixed long before SIMD comes into play. Reduce obvious DB/server round trips, bad data structure/cache locality, poor algorithm complexity, doing redundant computations, etc.
> Good news is that modern compilers are more than capable of emitting SIMD code even if original source is nothing but.<p>They're really not (I have a whole section on it in the blog post). This example in the post doesn't auto-vectorize, for example. And its a pretty big part of the overall throughput for plain text runs (ascii or unicode). Really, the point of that section is that almost nothing auto-vectorizes, backed up by LLVM docs and published research.<p>Instead, writing 12 lines for a 5x gain is way easier than crossing your fingers and hope someone else pays your bills.<p>Bigger picture, the real point is that this stuff isn't complicated. You wouldn't copy and paste 100 lines because you hope the compiler "lifts this into a for loop", you just write the for loop cause you know how and its simple.<p>Similarly, the common case of "process N values in parallel" is very simple. Write a dozen lines of code you're comfortable with. No need to pray the compiler people saved your bacon.
>Good news is that modern compilers are more than capable of emitting SIMD code even if original source is nothing but.<p>This is only true if you are intentionally writing code that the compiler can easily vectorise. Which is not most code.
> Reduce obvious DB/server round trips<p>In my mental model of optimization, the above and SIMD are basically the same thing.
There is nothing to fear or respect about using SIMD. Don't lionize learning. You can learn too, if only you allow yourself to.
Here's some important accompanying advice:<p>* If you aren't profiling, don't bother<p>Otherwise, you are going to be wasting plenty of time on "optimizations" that don't actually do anything useful.<p>Just come up with at least a few test that represent important cases and time them. Dig in, see where the time is being spent, and focus on areas where significant time is spent, especially ones that look ripe for optimization.<p>Also:<p>* Think about laying out your data in ways that accommodate your access patterns and are cache-friendly. SIMD would typically follow from this, not be a starting point on its own.