Runtime check for AVX2 #310
Labels
No labels
announcement
bug
build
CI
dependencies
documentation
duplicate
enhancement
experience feedback
feature request
github_actions
help wanted
install
invalid
Long-term
optimization
portability
question
target-specific
test issue
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-Cyan4973/xxHash#310
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
I was looking at experimenting with XXH3 and ccache when I noticed that there doesn't seem to be any runtime check to verify that the CPU actually supports e.g. AVX2. Even if the compiler supports it, it's not certain that the CPU does. Or the CPU supports it but it may be disabled by the OS.
I would assume that such a runtime check is needed to be able to enable e.g. AVX2 support in a generic binary.
I've done some trivial runtime dispatching using
__builtin_cpu_supports("avx2)which may be an alternative but have some limitations (see e.g. this comment).For a better example, see blake3_dispatch.c for details on how BLAKE3 handles this.
Why not use a function pointer?
I had something like this:
We have to find the right place to break off which allows constprop (or not?)
See https://github.com/easyaspi314/xxHash/tree/multitarget-v2.
The most important opened question when we looked at it some time ago was :
"where does it make sense to make this selection" ?
My position was that it makes sense to add this runtime detection into
xxhsum.c, aka the CLI program,but I'm less enthusiastic about forcing its presence inside the core library itself (i.e.
xxhash.c, now effectivelyxxhash.h), as I can imagine many scenarios where the presence of a runtime detection mechanism is unwelcome.Looking at the Blake3 example, it seems something equivalent could be done.
It requires the creation of an additional dispatch level, either part of its own separate
xxh_dispatch.cunit, or as part ofxxhsum.c, which could embed the detection test and then dispatch to the appropriate variant. Each variant can be compiled either as a separate object file, with the help of compilation flags andXXH_NAMESPACEto rename symbols and avoid duplication, or by relying on inlining and enforcing bytecode with compilation flags directly within the source code (slightly more complex, requires that inlining is enforced. note: this is the wayzstddetects and enforcesbmi2at runtime).I'm fully opened to progresses on this topic, the sole condition is that the detection mechanism must remain on the
xxhsum(program) side.So you are saying that we should only dispatch in the xxhsum binary instead of the library?
Seems kinda pointless for a library.
Also, may I bring up the idea of splitting up the source files again? We have three >70 kB files with 1.7k lines each, and 4 completely different hash functions which don't actually share a lot of code. (The algorithms are similar, but aside from a few inlines and
XXH3_hashLong, they are independent)Even if we duplicate the things like
kSecret,XXH3_update, etc, most people will be either inlining or using a static library (which benefits from split object files, so aside from a shared library and xxhsum, it will allow smaller code and faster compilation.Both questions go actually hand in hand.
How will the library be used ?
Sure, a program can link to the dynamic library
libxxhash, and this is probably fine for check-summing. But for many usages, especially involving small data, the library will have to be inlined, for adequate performance. So integration is often at source code level.And now it makes the integration experience fairly critical to the success of the library. Having just one file to include in any project considerably simplifies this step, and that's a major benefit that should not be under-estimated.
That's why the target is a single
xxhash.hfile, with a companionxxhash.c(when inlining is not required).xxh3.his a temporary add-on, to underline that it is in development, but it will be merged when stabilized.Now, I'm aware that it makes
xxhash.ha long file, if not a very long one, and that's indeed a pity. If the project was self-contained, yes, sure, this file could be separated into several smaller ones. But the integration experience matters a lot, and I don't take a chance on this topic.Now, it's also a reason to properly define what's the content of this un-splittable "core", and limit it to what is essential. Anything else can be outside of this single-file policy.
xxhsum.cfor example doesn't need to be a single file. It can be separated into multiple components. It doesn't matter as much, since it's primarily a self-contained project, delivered ready-to-compile with its own build scripts.And by the way, the component in charge of detecting cpu capabilities and routing invocations to the proper bytecode ? Yes, it's part of
xxhsum"domain", but it can be its own separate unit.Now that's interesting because it allows a user to integrate a custom scope, larger than just the "core" one. To simplify :
A user could select to embed both the core library and the bytecode selector into its own program, the same way
xxhsumwould do it.The important part is that integrating the selector is opt in. This design choice is necessary because I'm pretty sure that a commanding share of datacenter applications will not want it, and they must be able to include
xxhashwith the safety that the selector is not part of the deal.And of course, it should be possible for those who want it. It's merely an extended scope.
Even we integrate xxhash by inlining header instead of linking library, we still need this feature. Some CPU feature are the super set of another, and share most common instructions, such as SSE -> AVX -> AVX2. In reality, a distributed services is built once and deployed everywhere. If some of machines are AVX and others AVX2, we have to compiled the service as AVX/SSE which lost performance on new hardwares. The CI matrix is already very large, so many teams cannot afford to build different binaries for each X64 sub features. With runtime check, the same binary can dispatch the calculation to the proper branch, or fallback to scalar version.
The nice thing about this is that we don't need the scalar version on x86_64 and aarch64 — SSE2 and NEON are required by the architecture.
But yeah, this needs to be looked at.
I agree.
And I believe it's compatible with the suggestion above to create an "overlay" library, that would encapsulate
xxhash.h, and enforce the dispatch based on runtime detection of cpu features.Such overlay would be integrated into
xxhsum, but could also be integrated into any other application looking for equivalent capabilities.@easyaspi314 , I know you wanted to create an AVX2/SSE2 dispatcher some time ago,
do you still have time and want to work on it ?
If not, I would be able to start something before the end of the week. Just trying to avoid silent effort duplication.
I mean all I have is time thanks to being stuck in the house. 😩
I can work on it.
So this is the plan I am thinking of.
XXH_DISPATCHflag which replaces thehashLongfamily with an external function pointerXXH_COMPILING_DISPATCH, which definesXXH_INLINE_ALLand only makes an external symbol forhashLong.xxhash.ca few times with different flags.hashLongfunction pointer inxxhash-dispatch.cEither way, it will be ugly, though.
Dispatching the entire thing will be completely redundant and slow down short hashes.
I'm sure it can be made to work this way.
That being said, I'm afraid a solution based on a combination of complex build system + source support will indeed be painful to deploy, and even more painful to integrate into a 3rd party code base.
I would prefer a solution entirely based on source code.
And I believe it's possible, because we basically achieved just that in
zstd.Code example, for reference : https://github.com/facebook/zstd/blob/v1.4.4/lib/decompress/huf_decompress.c#L77
The basic design works this way (pseudo-code) :
OK, real code requires some more boiler plate, but you get the gist of it.
Advantage : no code duplication. No multi-includes. Fully source code driven. No dependency on complex build procedure.
Disadvantage : tied to specific compiler implementations: the scheme depends on the ability to send a COMPILER_ATTRIBUTE_xxx directive at function level. And requires
function_body()to be fully inlined.Lastly,
g_featureshould be a localstaticvariable initialized once, as querying the cpu about its feature flags is a long operation.Suggestion for discussion.
Also to be noted:
arm_neon.handaltivec.hphysically cannot be included without NEON/Altivec support enabled entirely, and shutting off NEON is not something Clang likes to do, the primary issue I see with this approach.Well, there are limits I guess.
For example, removing
SSE2support forx86_64target doesn't end well withclangeither.But I'm not sure that's an "additional" problem. If
clangdoesn't want to compileaarch64withoutNEONsupport, then it doesn't want to. No other solution can fix that either.Besides, we don't have to be "complete" on first try. I presume that the most important request is the ability to automatically invoke
AVX2when it's available. This could be a good enough use case to start an implementation, and then update it later to support more platforms.Can't you use the indirect function resolver for GCC/clang instead of manual dispatch? That happens once during loading which is friendlier when integrating into other tools.
Might be an option instead of mucking with complicated inline assembly routines.
We would have to do a
xgetbvcheck for AVX2 because some GCC versions don't check for OS support, but it would just be something like this:The only issue is that MSVC doesn't support it.
Wait, you mean ifunc
That doesn't work on Windows or macOS.
however, I think we can do this
This would work on Windows, but it would add globals which might not be desired.
writing a single byte will always be guaranteed atomic on all arch, and
xxh3_vtablecould be alsostatic, whileXXH3_dispatch()is still executed almostO(1)times.See #358.
Should be fixed with #387
ifunc wouldn't work with MSVC but clang-cl, clang & GCC should work so it might not be so bad to have MSVC vs everything else. I see you went with manual runtime dispatch though so probably not worth it.