The Case of the Missing SIMD Code
optidash.ai
optidash.ai
I'm sure libpng is in a better place with his improvement but I don't think not already having it is purely because they're dummies or "not ambitious enough".
In a world where profile-guided fuzzers are getting better every day, maintaining a high-profile library in a non-memory-safe language can be nerve-wracking enough with just a single implementation of your array-processing loops.
No relation with the project, just a user.
I don't know what a good way to compare these might be, other than perhaps activity/contributor count.
[1] https://github.com/simd-everywhere/simde
[2] https://github.com/ermig1979/Simd
[3] https://github.com/google/highway
I think this is inherently a language-compiler problem. When libraries are using intrinsic functions to override compiler decisions, they interact poorly. One example: here's comparing a scalar versus an avx-512 intrinsic version of accumulate.
https://godbolt.org/z/cMafh4Pnv
IDK if you can read assembly, but the intrinsic version is waaaay worse, and about 4x slower. This is because using intrinsics makes it very hard for the compiler to do other optimizations it otherwise would (e.g. unrolling and reordering).
Interestingly, even `#pragma clang unroll(4)` is ignored here. Often in such cases we have to manually unroll, e.g. introduce multiple accumulators.
A hopefully unbiased commentary:
Simde allows you to take existing nonportable intrinsics and get them to run on another platform. This is useful when you have a bunch of existing code and tight deadlines. The downside is less than optimal performance - a portable abstraction can be more efficient than forcing one platform to exactly match the semantics of another. Although a ton of effort has gone into Simde, sometimes it also resorts to autovectorization which may or may not work.
Eigen and SLEEF are mostly math-focused projects that also have a portability layer. SLEEF is designed for C and thus has type suffixes which are rather verbose, see https://github.com/shibatch/sleef/blob/master/src/libm/sleef... But it offers a complete (more so than Highway's) libm. Eigen is similarly useful for higher-level linear algebra that you probably do not want to reimplement yourself. However, I am skeptical of its expression template approach, in my experience one can run into trouble when the code gets complex (maybe some compiler limits are reached). Both of them may be less useful in non-math domains, AFAIK they lack many of the specialized instructions Highway has (e.g. interleaved load/store, compress/expand, software AES/CLMUL, popcount, lzcnt, saturated add/sub, 128-bit compare/minmax, fixed-point mul). I think Eigen would struggle with dynamic dispatch because of its header-only style, and SLEEF (understandably) lacks 8/16-bit types.
Highway is used in multiple projects and because it is just a collection of wrapper functions around intrinsics, it is more predictable/reliable. For this reason it is also currently AFAIK the only solution that supports scalable vectors in SVE and RISC-V V. The main downside is that it requires C++11, and so we are unable to help projects that are C-only and cannot compile parts of their code as C++.
I've run across: https://github.com/aff3ct/MIPP but have not worked with it extensively yet. It looks to be a solution to the rewriting X parallel pipeline into Y SIMD extensions.
Perhaps something like this, or languages introducing something similar into their standard libraries/modules would be a solution.
None of this of course solves the run-time detection of capability/growing binary size to support such.
This is also a purely library solution, so no build system magic is required.
I've seen this story many times before. What the author should be doing is reorganizing his code to be branch-free, clear about aliasing, and aggressive with vectorizing-friendly pragmas. Notice that when he wrote vectorized code in the 25-year-old SSE instructions, the compiler was able to optimize into better, more contemporary SIMD instructions? It's not because he used SSE intrinsics. It's because SSE intrinsics forced him to write branch-free code. If he took the same steps with scalar code, it would now be clearer and easier for the compiler to truly optimize his code, and it would be stable throughout time as new architectures come out.
Wait, what? x86 has specialized instructions for all kinds of things but doesn't have absolute value?
https://en.m.wikipedia.org/wiki/X86_Bit_manipulation_instruc...
Popcnt, LZCNT and TZCNT are pretty nifty and all, but probably more obscure than absolute value.
I do think pdep and pext are brilliant and that more people should play with those two BMI2 instructions.
Amdahl's law. You don't gain a lot of benefit by improving something which is already fast.
TZCNT being used for string parsing... Sorry, can't see it at all or how it could be possible lol. Could you give an example?
(31 - LZCNT(x)) is binary 32-bit logarithm and likely has a number of mathematical applications.
If we want to keep the list of positions, there are other approaches for converting the vector mask to the list of positions. We could and the vector mask with {0,1,2,3,4...}, then widen by 4x to get int32 positions and add our current int32 position, then use or emulate vcompress and unconditionally write everything to the list, counting on updating the position by popcnt(mask) to keep the array dense. So we'd still need popcnt, but some implementations of this sort of thing end up computing this some other way[1] and don't literally use the popcnt instruction. This approach might be more reasonable if we only widen by 2x and produce a list of int16 positions per 64k input chunk then go back and widen that whole list to int32 later.
[0]: https://github.com/simdjson/simdjson/blob/master/src/generic...
[1]: https://github.com/lemire/despacer/blob/master/src/despacer....
BLSI and BLSR are better instructions for iterating over a bitset and finding the 1s (BLSI) and then removing the 1s (BLSR).
Furthermore, it only took 3 assembly instructions to implement before the BMI instruction set (see: https://www.chessprogramming.org/General_Setwise_Operations#... )
You're correct on popcnt, but this kind of iteration I normally see with BLSI / BLSR, not with TZCNT. I feel like TZCNT is slower and less elegant for this purpose.
---------
To go from a single binary bit selected (ex: 0b00001000) into its index (ie: 3 in this case) I guess TZCNT could be used though. But I would have done popcnt(x-1) instinctively.
TZCNT would be faster though, so there's that.
------
But all of these operations are only ~2 to 3 assembly instructions. Roughly on the same scale as absolute value. With exception of popcnt of course.
----
EDIT: Okay, I think I see how TZCNT comes into play now. Thanks! Just had to think it through.