You probably shouldn't use a lookup table (2022)
specbranch.com
specbranch.com
I ran into a similar problem in another program where I needed 128-bit integers. I implemented them myself as a C struct containing two 64-bit integers. Then I discovered that GCC implemented its own 128-bit integer type. But that type was somehow slower than my implementation!
[1] https://stackoverflow.com/questions/52161596/why-is-builtin-...
We found this out, when we were optimizing image processing pipelines (image processing uses lots of lookup tables).
We found that the best way to optimize was to avoid moving out of cache, so we had to rewrite a lot of our stuff, so it never broke cache.
That meant a lot of copy-and-paste repetition, strict (small) static functions, and hardcoded values, instead of properties or even constants.
I don't remember all the stuff we learned, but it wasn't pretty (and applied only to x86 architecture. Not sure how well it would work, these days, on ARM).
Meanwhile, divide is insanely slow.
If your table is in L1 or L2 cache, it should be faster than the divide, but if not, I’d guess it would be slower. As the article says, that’s why you test then hope it doesn’t change later.
Things like the Ps2 VPU1 16KByte cache and the much more limited VPU0 with 4Kbyte cache. There were many titles that just did not use VPU0 for anything high performant simply because the cache limit killed its potential.
What's the difference in the object code between a hardcoded value and a constant? Surely they should be the same?
It was C++ (LLVM, I think).
It was important that the PC never leave the cache address pool, and constants may have been in a table, somewhere.
With macros, it was almost the same.
Later compilers added an explicit zero of the output to work around this issue.
So for example, you may have a bit of code that is rarely very hot unless you're being DOS attacked. If you are it's performance is limiting for the system, so very important.
Otherwise it just runs intermittently. A large table may win in a microbenchmark or under the dos load, but trash the application performance outside of attacks due to flushing out the cache relative to a slower implementation with a smaller table or no table.
It's often difficult to measure the overall application performance difference between changes to components (as effects from alignment may dominate). And even if you do know that you have a situation like I described the correct choice isn't always obvious.
It's still a simple rule to find out by testing, but the simple rule is not so limited.
The initial version of the word cache cleared the entire cache when it hit a certain number of entries, just as a placeholder. When I got around to adding some LRU eviction logic, it became faster on our desktop simulator, but far slower on the embedded device (slower than with no word cache at all). The difference came down to the CPU cache size / strategy differences between the desktop and mobile CPUs.
We ended up shipping the "dumb" word cache logic because it was so much faster in practice. The word cache eviction function was only two lines of code plus a large comment that acknowledged that "yes, this looks dumb" and imploring that the speed of any future improvements be compared to the dumb version on the target device. Testing real data on the hardware used in practice is incredibly important.
The correct way to compare these approaches is with real code.
But I also understand that you can't do that for every decision you need to make, so at least this article gives a hint that lookup tables are something that might be worth measuring.
You can compute a ton on a modern CPU during what a memory access would take.
IMO: the bottleneck of lookup tables is concisely described as follows: 2 or 3 lookups in L1 cache per cycle on modern CPUs, and 10x fewer to 500x fewer depending on how far away (L2, L3, DDR near, or DDR remote).
Meanwhile, modern CPU instructions like AES, Multiply, XOR, ADD and more can effectively operate 3, 4, or even more times per clock tick regardless of circumstances.
--------
Don't even talk about cache hierarchies. You are already behind at the L1 cache level due to the relatively few load/store units in modern CPU (or GPU) cores. And hitting L2 or L3 cache gets exponentially worse.
I don't think people are making lookup tables to adding or xoring 2 numbers...
Pshufb (4-bit / 16-byte lookup table)
You might be surprised at how many operations can be done without touching the load/store units on CPUs.
In my experience, it's usually worth the effort. Then again, it's a LOT of effort needed.
If you are just doing things simply and easily? Lookups are almost the easiest solution. But I probably wouldn't reach for lookup tables for a performance based problem today.
The function to do the same for a byte is... what? 3 or 4 lines long in C? I guess it takes a couple of temp variables, though, which may dissuade some devs.
auto is_upper= (x > '@') & (x < '[');
return is_upper ? x+32 : x;
Which boils down to add, add, cmp, cmov on x86. It might save some cache traffic, but the cache traffic is less code since it's only a mov. The version that's code instead of lookups probably exacerbates register pressure problems in large programs? I don't have good intuition for which would be more efficient in practice.I dunno, it's ridiculously a benefit to the code by my instinct. While lookup table looks pretty bad.
I mean, I'm not skilled enough in those ISA extensions to stick my neck out. It's not totally obvious to me that there is not some shuffle or permute facility that can load 64 bytes at a time from LUTs.
You are talking about vgather and/or vscatter, which are well known to be very slow AVX2 or AVX512 instructions.
Maybe a future CPU will make these instructions high performance. But no modern 2023-era CPU has a high-speed vgather.
Like: the vgather does one-at-a-time slow. You basically lose parallelism even if the vgather instruction describes what you want to do, it's not an effective parallel operation today (and may never be)
--------
Pshufb as a 4-bit LUT is an exception and is effectively a high speed (but very very small) lookup table. Like every cycle 64 bytes at a time fast.
You are limited by the 16-byte lookup size (aka the size of an SSE register), maybe a bit bigger if there are new instructions I dunno about.
By using SIMD instructions like AVX2 you can do 32 characters in parallel. With AVX512, 64.
Pretty easy to write something way faster by not using a LUT.
In fact, I'm pretty sure that Unicode cannot be solved with a lookup table due to its variable length, but maybe you can prove me wrong? (Assuming UTF8 here)
Unicode does have a limited space, but it cannot be stored practically in single table. It currently runs up to 0x323AF, a bit over 200k, and most of the characters of course don't have a lower/uppercase mapping. The implementations I've seen do a few comparisons and then delegate to a table.
But: horses for courses. If you have to normalize a lot of Latin-1 text (such transform case, or strip diacritics), you can probably write some vector instructions that runs circles around a simple LUT. But it's not going to be as easy.
TIFFNoBitRevTable[256] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, ... 0xFF }
Maybe PSHUFB/VTBL to reverse each nibble and then put the reversed nibbles together would be wayyy faster?I'm guessing portability matters here, and there is not much commercial pressure to actually optimise this TIFF library?
The most used one is probably sin() on CPUs without hardware math
But 4-LUT is also called pshufb and you never ever need to touch the relatively slow load/store units.
The space of computations you can do with 4-LUT PSHUFB instructions, followed up with simple bit instructions (add, or, not, xor, multiply) is rather insane.
The developers of Mario 64 did the same, but this video dives into those LUTs and brings them into question: