1. 133
    Everyone Should Know SIMD mitchellh.com
  1.  

    1. 11

      I guess everyone should know SIMD but I would argue that not everyone needs to know SIMD. Most programmers work on much higher levels of abstractions where this is not only not needed but would also constitute as premature optimization.

      1. 13

        I think it's still valuable to know what SIMD can and can't do, at least. Even if you're programming in a high-level language, the relative performance of the primitives you're using still depends on how they're implemented. Simply knowing that something is optimized with good SIMD tells you that it likely has the same performance on 1-16 bytes, that you might get a small benefit from data alignment, that the length of the input in bytes determines performance more than the raw count (e.g. if you can choose between 8-bit and 16-bit values), and that compiling for AVX instead of using the default settings will likely double your performance. That's already pretty useful!

        1. 9

          What I'd like to see is better compiler warnings, and optional warning levels that note optimization misses. Specifically in this case, when the compiler fails to auto-vectorize a function or loop, print a note explaining why, and suggest a code change that would make auto-vectorization succeed.

          SBCL Common Lisp compiler will print notes when it can't apply an optimization, and it's really handy. Although, TBH, a few of the notes can be cryptic.

        2. 7

          I always feel like we should strive to learn how computers work if we're working with computers.

          1. 1

            Yes, and that's what I mean with the difference between "should" and "needs". A good programmer should strive to know more but you can still be a good (even a great) programmer if you don't know about SIMD (or some other thing). There is so much more than just performance, compilers and, optimizations that makes a good programmer! And obviously it all depends on the domain you work in.

          2. 3

            It's a fair argument, but at the same time it's always useful to learn a bit more than what you normally do (or what you think you need). That's how one could expand their horizons and learn new valuable skills. Fun fact: you'll never know when you might need that skill and learning more can only make you better, not worse.

          3. 8

            Feel like Zig make this more pleasant to work with than some other languages.

            Sometimes I contemplate using SIMD but then I wonder:

            • Is there some restriction on the buffer alignment?
            • How to build my software once and run it on heterogenous hardware? CPUID dispatch does not seem straightforward.
            • I'd often want to bench/test the SIMD implementation against the baseline implementation but it means they need to coexist at runtime.

            Well, picking the baseline target at compile time may not be such a bad idea after all, it makes writing SIMD straightforward.

            1. 2

              Yeah, I'm stuck in C and whenever I've looked into SIMD I've quickly put it back in the too-hard basket. I've not seen any helper libraries for C like there are for C++ or other languages, so I guess I'd be stuck using architecture-specific intrinsics directly and I'm not sure any one loop in my app is enough of a performance bottleneck to warrant that sort of work

              1. 5

                The GNU C portable vector extensions can be useful if you don’t need the fancier intrinsics. Previously, for instance

                1. 4

                  I wrote SIMD accelerarion for the HTML parser in Firefox using GCC/clang portable SIMD with a specific operation in the mix using vendor intrinsics, but it seems to me that it’s rare to be allowed to use GCC/clang portable SIMD.

                  So how much effort goes into abstraction layers that pretend that only the vendor intrinsics exist, when the vendor intrinsics actually map to a cross-ISA layer in the compiler.

                  I wish Rust’s core::simd was shipped to non-nightly Rust so that Rust crate authors didn’t feel the need to pretend that vendor intrinsics are the compiler reality.

              1. 6

                Tangential to the article, but I think it's so cool: if you find yourself forced to use pure Python, but wishing you could also do fast data processing with SIMD, you'll love this innovative technique by @retr0id.

                The summary with spoilers is that SIMD is used in CPython for integer arithmetic, so if you pack your data into big integers, it will use SIMD under the hood for fast processing.

                1. 1

                  That is magnificent, @retr0id, hats off!

                  Many years ago I did a bitsliced SWAR implementation of Life, and later Tom Rokicki told me how to make it faster. Compared to retr0id’s version, mine packs the cells into one bit per pixel (instead of 4) so it needs less memory bandwidth, but needs slightly more ops per generation. (I wonder how that tradeoff might balance in Python SWAB.)

                  I also found it hard to render at a decent speed. (It was complicated by my run-length compression of empty space, which makes Life faster to calculate but slower to observe.) IIRC OpenGL with GLUT did the job reasonably well without too much effort.

                2. 2

                  One thing I really don't understand in the example is why it's using u32 for each lane when u8 is enough precision and range for all the steps? @mitchellh?

                  1. 7

                    The post says the values are codepoints, maybe this is after UTF-8 decoding?

                    1. 3

                      Aha, I missed that. Thank you!

                      (Obvious question then is whether maybe this loop could be made to use less memory bandwidth by doing it on utf8 bytes before decoding, but that depends on the program's structure.)

                  2. 2

                    Input sizes in relation to SIMD vector sizes remains a tricky issue though. In particular, SIMD isn't really worth the trouble unless your input sizes are at least one multiple of your vector size. The smallest vector size today (that I know of at least) is 128 bits/16 bytes.

                    The reason for this is that for inputs up to 16 bytes you can instead read/process them as integers of different sizes (128 bits, 64 bits, etc), i.e something like this:

                    while size(input) >= 16 {
                      value = read(input, bytes: 16) as Int128
                    
                      ... do something with the value ...
                    }
                    
                    while size(input) >= 8 {
                      value = read(input, bytes: 8) as Int64
                    
                      ... do something with the value ...
                    }
                    
                    ... repeat ...
                    

                    This is for example how Inko's equality for strings and byte arrays is implemented (see here), though Inko certainly isn't unique in this regard.

                    Even if your input size is large enough to fit into a vector, I think that due to the setup cost (i.e. loading data in and out of vectors) of SIMD you realistically need at least two vector's worth of input. So if your target vector size is 128 bits, ideally you have at least 256 bits of input to process.

                    Apart from that, I think the issues that hold SIMD back today are:

                    1. The mess that is different CPUs supporting different vector sizes, or newer CPUs not supporting something, or it causing your CPU to overheat
                    2. Compile-time checks for different vector sizes being a pain as a result, while runtime checks are a pain because you have to compile the same code a bunch of times (something generally not well supported and thus requiring lots of manual plumbing), in addition to runtime checks being potentially more expensive than the work to perform, depending on where the checks take place
                    3. SIMD instruction documentation being the equivalent of "haha go screw yourself", the names alone should be enough to send those who came up with them to jail
                    4. A lot of SIMD algorithms are in fact technically unsound in that they often read beyond buffer boundaries, something that mostly works by coincidence but is in fact very much unsound/relying on undefined behavior. Translating those algorithms to something that is sound is often a challenge

                    I collected some more findings on this for Inko in this issue, with my conclusion being that at this stage I'm just not sure how to make it work without ripping my hairs out, though part of that is due to how Inko itself works and not just because of SIMD.

                    1. 4

                      SIMD isn't really worth the trouble unless your input sizes are at least one multiple of your vector size.

                      I found that the masked loads and stores in AVX512 can make it reasonable to use SIMD for quite small amounts of data: the crossover point for ASCII tolower was 5 bytes in my testing.

                      1. 2

                        SIMD instruction documentation being the equivalent of "haha go screw yourself", the names alone should be enough to send those who came up with them to jail

                        That's just x86 instructions in a nutshell though, PUNPCKHDQ fits perfectly right next to CMPXCHG16B.

                        1. 1

                          you realistically need at least two vector's worth of input

                          You bring up many good point, but I'll disagree with this one specifically. I've found SIMD every useful in cases where the data is already stored in memory and the number of operations to do is a small variable. If you write a simple for loop, you're paying for (rare, but constant) branch mispredictions on every iteration. If you use SIMD, you can process 16 values at once and then mask them to the right amount, with much fewer mispredictions and better performance as a result.

                        2. 1

                          I don't know how effective it is, but Julia's @simd macro is a fantastic interface. Just write a normal loop and put it in front!

                          1. 1

                            Great article and something I was curious about when exploring the ghostty codebase. Also while exploring the foot codebase I noticed they use PGOs for compiler-optimizations. It looks like zig doesn’t support PGOs at this point but I’d be curious if ghostty could get some gains out of employing a similar optimization strategy on top of simd

                            1. 1

                              Neat article, thanks! One question:

                              Could this:

                                      if (@reduce(.And, greater_than_threshold)) continue;
                                      const mask: std.meta.Int(.unsigned, lanes) = @bitCast(greater_than_threshold);
                                      end += @ctz(~mask);
                                      break;
                              

                              just be simplified to:

                                      if (@reduce(.And, greater_than_threshold) == false) break;
                              

                              which then lets the final loop grab the index of the interesting item. (apologies if that's not valid Zig but you know what I mean)

                              Or is that clever ctz/bitmask so much faster than a for loop going over, at most, <lanes> items that it's definitely worth it?

                              The reason I ask is that for the purposes of this article, trying to demystify simd, it would make things much simpler.

                              1. 3

                                I was curious and looked into it.

                                The final scalar loop would determine the exact index. Whether the mask/ctz path is faster is probably very architecture and workload-dependent. On Apple ARM, NEON has no native movemask and when I looked at what the compiler does, Zig lowers the mask construction to several instructions. I had an agent do a simple four-lane M5 benchmark and scalar rescanning was faster for a failure in lane 0, approximately even for lane 1, and slower for lanes 2-3 (by up to 10%).

                                I'm assuming that on x86, where mask extraction is generally cheaper, the mask/ctz approach is more likely to win.