1BRC merykitty's magic SWAR: 8 lines of code explained in 3k words
questdb.io
questdb.io
More than 2 years ago I found that byte array view var handles are quite suitable to cook efficient SWAR routines with Java/Scala.
See a lot of other examples of SWAR usage, like parsing Base16/64 strings, java.time.* and number values directly from byte arrays:
https://github.com/plokhotnyuk/jsoniter-scala/blob/master/js...
(Not quite interesting enough for me to do it myself though ;-)
In addition, the official 1BRC explicitly evaluated results on a RAM disk to avoid I/O speed entirely: https://github.com/gunnarmorling/1brc?tab=readme-ov-file#eva... "Programs are run from a RAM disk (i.o. the IO overhead for loading the file from disk is not relevant)"
1. Cloud VMs sometimes have very slow access even to devices advertised as local disks, depending on which cloud
2. Windows
The latter often catches people out because they develop intuitions about file I/O speed on Linux, which is extremely fast, and then their app runs 10x slower on Windows due to opening file handles being much slower (and sometimes they don't sign their software and virus scanners make things much slower again).
Whether it'll be constrained to Win 11 is yet to be seen.
MS does have something called "dev drive" in Win11. Dev Drive is what ReFS turned into and is an entirely different filesystem that isn't NTFS, which is better optimized for UNIX-style access patterns. The idea is that core Windows remains slow, but developers (the only people who care about file IO performance apparently) can format another partition and store their source/builds there.
From my limited understanding, we sequentially bring a large text file into L1 and then do a single read for each value. On most processors we can do two of these per cycle. The slow part will be bringing it into L1 from RAM, but sequential reads are pretty fast.
We then do some processing on each read. At a glance, maybe 4 cycles worth in this optimized version? Then we need to write the result somewhere, presumably with a random read (or two?) first. Is this the part you are thinking is going to be the I/O bottleneck?
I'm not saying it's obviously CPU limited, but it doesn't seem obvious that it wouldn't be.
Edit: I hadn't considered that you might have meant "disk I/O". As others have said, that's not really a factor here.
It's quite a bit more than that, just the code discussed in the post is around 20 instructions, and there's a bunch more concerns like finding the delimiter between the name and the temperature, and hashtable operations. All put together, it comes to around 80 cycles per row.
When explaining the timing of 1.5 seconds, one must take into account that it's parallelized across 8 CPU cores.
"Programs are run from a RAM disk (i.o. the IO overhead for loading the file from disk is not relevant)"
Why is that a mystery? There's still people out there who actually know how to program a CPU and actually understand what they're doing. The real mystery is the lack of deeper understanding of most people who call themselves programmers, combined with the fact that they don't appear to be knowing that they're seriously lacking.
Evidently, it works really well which can be observed on the example of a C# solution being the fastest at 1BRC that has been publicly seen so far: https://hotforknowledge.com/2024/01/13/1brc-in-dotnet-among-...
The question is if the cost of setting up the initial vector and extracting the result isnt prohibitive.
PMULUDQ is in SSE2, though I haven't checked if that's usable for the problem here. There's also PMULLD in SSE4.1 if you only need a 32-bit result. But for summing digits, perhaps SSE2's PMADDWD could be sufficient?
Completely forgot about pmuludq, that works too for SSE2. But a 32-bit result is insufficient for the magic number method, needs to be at least 36-bit. I originally used vpmaddubsw+vpmaddwd, but switched to vpmuldq for the reduced register pressure, and I was already only parsing 4 numbers in ymm registers so the 64-bit result didn't affect me (after parsing 4 temperatures I immediately did the respective hashmap stuff for each).
SSE was successfully applied to finding the delimiter between the name and the temperature, though.
But Java doesn't really support SIMD use of the CPU except in a very few specific cases, which is sad.
If (unlike the solutions that won this challenge) you would like to use a hash that consumes all the bytes in the input, then scanning for all the delimiters and bucketizing strings by length also lets you avoid branch mispredictions caused by random lengths.
I believe you can't get away with just 16+16-bit per state transition case - you have to validate that the input is what the entry was for, and your proposed 16-bit fields don't include anything for that. (having a full 2^32-element table won't help you, and would be a massive 16GB by itself anyways).
I don't see how 2^16 counters can work, as there are clearly 400×400 = 160000 different name×temperature combinations to track; lowering to 160 temperatures is awful. (maybe you could squeeze that together with the output state bits though, which are somewhat fewer, but doesn't help much).
Which leaves with just the option of increasing to 8 bytes per entry.
That ends up as a massive table as e.g. a window of "4\nAb" has 400×40 possible input states, and is 10×141 cases, for a total of 22560000 transitions, 180MB; and there are more than just this window, and you need some empty space to reduce collisions (though some enormous L3-s maybe could still cover it. maybe.)
The current best native solution works at ~2 cycles per input byte per core, so your method per 4 bytes has to beat 8 cycles/element. Your described solution is (cycle counts on Intel, as Zen 4 has much slower gather/scatter):
1. gather the next block of 4 bytes for each element; let's say 0.33c/elt as these are all in L1.
2. gather 8-byte values from the state transition table; as the table is massive and random access, that's limited by the RAM to cache interface; though you only need 8 bytes, the CPU will fetch 64B cache lines, so even with DDR5 at 64GB/s that's just 1B reads per second, or 3c/elt at 3GHz.
3. vpconflictd for when elements hit the same histogram bucket (~1.2c/elt on Intel; 0.08c/elt on Zen 4; also needs some fallback for when the collisions do happen (luckily unlikely if you mask out states that don't terminate the record); also means that these steps 3&4&5 cannot be interleaved without a separate histogram table for each).
4. gather the histogram previous value (0.5c/elt?).
5. scatter the incremented histogram values (0.7c/elt?).
Furthermore, to handle the rarer temperatures/collisions (the distribution of names is uniform so there's no point in handling only a subset of those), you need some fallback mechanism. Targeting 400 temperatures gives you a 5% chance of a failed lookup per entry (i.e., for a 16-element register, there's a 56% chance that at least one will hit a bad temperature), and you must restore the failed lane to a working one very fast, or otherwise all lanes will end up dying quite quickly. Some options:
6.a. do a scalar ctz+blsr loop over a mask of the failed entries (a bunch of cycles and very branch-mispredicty);
6.b. increase to ~600 targeted temperatures, for only 0.2% chance of a failed lookup (still 3.1% over a 16-element vector), and do a scalar fallback when it does happen (still somewhat mispredicty);
6.c. store all failed entries to a buffer (scatter, 0.7c/elt, probably even with most entries being masked off? could be wrong though); and then of course whatever code for actually handling these 5% of more complex entries.
So that's somewhere around 0.33 + 3 + 1.2 + 0.5 + 0.7 + 0.7 = 6.43 cycles per 4 bytes, not far from the 8 cycles of current solutions, and that's not counting any of the intermediate arithmetic, assuming ideal cache & RAM performance (and that the CPU can actually OoO over all of the RAM latency and not flop on the gather/scatter of the histogram not having known addresses for a while), and doesn't leave much for the table initialization.
But before you run to implement it as a drop-in replacement for strlen() all over your code, please note that in real-world cases the string length is not guaranteed to be a multiple of 8. When you read the last long, you might overrun your memory buffer and trigger a page fault.
You know that page boundaries are always on a `PAGE_SIZE` alignment, which you can get with `getpagesize()` or `sysconf(_SC_PAGESIZE)`.
...but overrunning the declared size of a memory allocation is still UB, even if you don't trigger a page fault, so YMMV ;-)
In fact, because the compiler is allowed to re-order your program provided that the end result of any non-UB computation is the same (the "as-if" rule[0]), if part of your program invokes UB, it's possible for that to "reach backwards" and invalidate computations that appear to happen before the UB is/was invoked (by a straightforward reading of the source code).
One of the important things about UB and C is that C is a language defined by a standard, not any one compiler - and there are a lot of compilers. If a C compiler does something weird with your source code, we can say whether the compiler is correct or not, or whether your code is at fault. For many languages, that is not the case, and "whatever the compiler does to your code" is de facto correct, and "whatever the compiled version of your code happens to do on any given platform" is well-defined, even if it wasn't what you meant.
I'd argue that if a compiled language does not define what it means by UB, it doesn't have "UB".
But also, if a person talks about "UB", it is reasonable for a listener to assume they are by default talking about the well-defined term in the C programming language standard - unless they specify otherwise. Because that's the most enduring definition of the term we have.
I can't imagine that has been efficient for decades tho
in pocket calculators you have to display the result in decimal after performing a single arithmetic operation, so it's much more efficient to do the arithmetic in bcd than to convert from decimal to binary, perform the arithmetic operation, and then convert back
But can't someone just ask merry kitty where they got the idea from?
long nextLineStart = ...
[1] https://github.com/gunnarmorling/1brc/blob/dfec2cdbe6a0334cf...
If the input wasn't in expected format (e.g. different decimal separator), the result would be incorrect.
Fun ss an exercise, but hard to understand, maintain.
Really not a mystery. This is not that complex for people with low level programming experience (embedded, (older) games, demoscene, performance systems, etc.).
Knowing these tricks and knowing how to apply them is basically a requirement on these areas.
There is nothing magical in computers once you can do that deconstruction in your head.
I think this is an excellent skills showcase, even if I don't think it's magical.
* More specifically, miniaturized robots consisting of one or more robot arms with multiple degrees of freedom, a CPU, some kind of storage (perhaps something like wound-up DNA serving as tape on reels), executing programs written by humans.
On a macro scale, life tends to be bilaterally or radially symmetrical, but our robots are not necessarily like that, just considering even factory robots which are often an arm and little else. So, at the micro scale, I don't think they have to resemble "life" there, either. I'm hardly suggesting some kind of tinkertoy with one atom here and another atom there and the strut being, instead of wood, a covalent bond. No, I think we would need more atoms than that.
Frankly, we haven't much tried to scale down our robotics. Oh, you'll find someone who will produce the flagellar motor (a mere forty-five nanometers, compared to the ten thousand nanometers of a human cell) but not much else. I wouldn't worry about the thermodynamics and quantum effects until we're down to that motor level.
By just taking standard equations found in textbooks, plugging in some numbers, and realizing that things such as Ideal Gas Law don't hold at such scales.
You can even derive the equations for yourself to confirm your not being misled by the textbooks, but that takes more skill and knowledge then is likely feasible to acquire without advanced study.
We will face different challenges at different scales, of course, but even as recently as 2016, researchers using a scanning tunneling microscope crafted a 8,192 bit message using less than ten thousand atoms. So, no, I see absolutely no reason why we couldn't have robots within an order of magnitude of the human cell -- we have bacteria which are much much smaller capable of a variety of functions as well as reproducing themselves
You might look into Feynman's "There's Plenty of Room at the Bottom."
Clearly cell-like robots are 100% possible at very small scales, there’s no one debating you on this.
I feel like this kind of knowledge is becoming a lost art these days despite having lost no relevance.
I had a question recently: would an extra 65th bit in some registers have a good use case with stuff with SWAR?
The much-hyped RISC-V is one of the few that doesn't have a flag register, because it's been claimed to be a performance bottleneck - in my somewhat heretical opinion, the actual reason is that its designers are deluded into thinking that C and UNIX are as fundamental to software as transistors are to hardware, and thus anything beyond what's needed to support that environment is not worth implementing.
But an extra bit per register would be like having a separate carry flag for each, potentially solving some problems with parallelization and also allowing checks for integer overflow at the point a value is stored into memory or used in a subsequent operation.
Having some kind of carry flag enables various programming tricks that can also be useful for SWAR, for example subtract-with-carry of a register with itself will either set or clear all bits.