[WIP] implementation of XXH3 dispatcher #358
No reviewers
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!358
Loading…
Reference in a new issue
No description provided.
Delete branch "dispatcher-xxh3"
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?
Very rough and untested implementation for now. Might upload binaries later.
The following functions should now dispatch on x86/x86_64:
Short hashes are not affected by this change, to prevent slowdowns when only working on short strings.
Makefile needs to be updated, for now it is compiled like so:
xxhash.o
XXH_NAMESPACEsupportIt will print what features it has detected to stdout for the testing period, looking like this:
The way it currently works is that it will compile the target objects with XXH_INLINE_ALL, therefore nopping the rest of the xxHash implementation, and emit a single symbol:
XXH3_dispatcher_table_<numeric value of XXH_TARGET>The dispatch table is abstracted away with a macro:
so for example,
XXH3_hashLong_64b_defaultSecretis called asThis will either expand to
Or
Noticed that I wasn't dispatching some of XXH128's functions.
On Windows, it successfully detects AVX2 on my Ryzen.
Also, using Intel's SDE program, I tested a 32-bit version targeting the original Pentium, and aside from complaining about a
cmovzin the CRT startup routine, it seems to work perfectly fine on the scalar path. It also seems to detect AVX512 when I target it.I'm going to test disabling XSAVE on my PC and see if it switches to SSE2, then I'll upload some test binaries.
With
it successfully selects the SSE2 codepath.
Here are the binaries and the shell/batch scripts for building it.
These include Windows x86 and x86_64 binaries generated by GCC 9.3, Clang 10.0, and MSVC 2019, as well as static Linux x86_64 binaries compiled by Clang 6.0.0 and GCC 7.4.0 (yay Ubuntu 18.04 LTS).
xxhash-dispatch.zip
Looking into this PR,
I see that the selected methodology is mostly build driven :
it requires
xxhash.cto be compiled several times with different intrinsic code paths.I suspect it will be difficult to integrate by 3rd-party applications.
I was wondering if integration could be simplified by avoiding the difficult build stage.
For example,
xxhash.hcould be included multiple times withinxxh3-dispatch.cusing different code path selection.For this to be possible, I presume it would require a combination of
XXH_INLINE_ALLwithXXH_NAMESPACE, so that each variant receives a different name. The dispatch can then select the proper variant at runtime.If that's a direction that sounds correct, I could put together a PR which allows combining
XXH_INLINE_ALLwithXXH_NAMESPACE, a capability that's not present yet.Well we could do what HighwayHash did, using stub "dummy" files:
https://github.com/google/highwayhash/blob/master/highwayhash/hh_sse41.cc
My reasoning for breaking off at
hashLongis to avoid the overhead of dispatching on short hashes on single shot. Most of the time you will be using it for a hash table, and that might never callhashLong.Theoretically, we could
XXH_INLINE_ALLthe primary implementation, and dispatch off from there. Haven't figured out the best way to do it, though.I think that's a good point. Dispatcher should only be triggered for long inputs.
This Makefile logic will detect the supported flags for GCC and Clang.
On GCC 4.2.4, it only compiles SSE2 files, as it doesn't support AVX2 or AVX512.
And the target clones can be implemented like this:
Should we also break off at
XXH3_digest_long? My reasoning is that most of the time, we are only callingXXH3_accumulate_512a few times, and it isn't the worst thing in the world if we take the scalar path.I was thinking of some alternative approaches.
First of all, we could use a function like this:
This has a few advantages:
XXH3_dispatch_table = evil_tablekDispatchDispatchertable and copy-pasted functionsHowever, it has to deal with the overhead of a function call and a branch.
We could also cache it like this:
This avoids a function call on subsequent calls, but will require local static variables for each inlining with
XXH_INLINE_ALL, which from what I am seeing, potentially blocks inlining. It also makes the macro wrapper impossible without some fancy GCC extensions.I like this last version.
Caching the result in a local static variable is something I was going to recommend.
I don't see any problem regarding inlining on my side (though of course I could always be wrong).
The only issue I could guess is a risk of double-initialization in multi-threaded environments, but even that is not a big deal, since initialization's outcome is presumed the same.
Self caching still works for
XXH3_get_dispatch_table.In addition of idempotency, also need to consider whether storing a function pointer is atomic.
That will still call a function each time.
If we didn't give a damn about compatibility and wanted to just use GCC extensions, I wrote a macro which would work with xxh3.h as-is:
So let's look at possible data race issues:
We can immediately eliminate condition 2 because the feature test will be deterministic on any sane system, so a torn write will still end up storing the same value.
On all known platforms,
sizeof(void (*)()) == sizeof(void *)Therefore, 1 and 3 can also be eliminated by the x86 and C standards:
This aligned store atomicity is also guaranteed by non-M ARMv6 and later (you can't combine A/R/M binaries, and NEON is only on A), aarch64, and if I am reading correctly, PowerPC and s390x.
Therefore, it is safe.
how about passing the runtime feature into hashLong_64b_defaultSecret and select the instruction branches via
switch .. case, this should only have one jumping cost:At entrance of
hashLong, we can still use self caching way to initialize thecpu_featurewith O(1) times. The caching values is a function pointer (as previous analyzed, this should be data-racing safe) ofhashLong_64b_defaultSecret. So another runtime cost is an indirect call, this could not be avoided in any runtime dispatching methods.When using GCC, we can merge
hashLong_64b_defaultSecret_checkandhashLong_64b_defaultSecret_no_checkinto one function, and initialize the cpu feature withlabel as valuesextension:Well that also requires declarations of all the functions, and will also increase code size, require fallbacks for non-GCC compilers which we have to support. It also requires conditionals for when we don't have AVX2 or AVX512, and will just look really ugly.
The self caching function pointer seems to be pretty well optimized with Clang, even with PIC:
Most of the time, we will be calling the same function over and over again, so the single caching seems ideal.
Hmm.
Trying to figure out the balance between sanity and readability.
I don't want to write this over and over:
So my idea could be this:
Is that readable/sane enough?
Life would be so much easier with
typeofand statement expressions... I could write this in half the lines of code.with
typedefwe could avoid passparamlistevery timeI totally didn't think of that. That would make things pretty simple with some token pasting magic.
Try to simplify the
XXH3_DISPATCH_RETURNmacro:We can't use variadic macros, those require C99. Besides, that would need more global symbols, and you can't do pointer arithmetic on function pointers.
The typedef solution works.
Hmm, QEMU, being quirky as it is, will actually return 0 cpuid leaves on
-cpu base. (Yes, I am running QEMU on my phone 😛)Will do some cleanup, add some more debugging code, and push soon.
Is there anyone who is good at CMake who can give the dispatcher code a shot? Because all I see is something hopelessly messy or verbose.
There are a lots of good things in this PR.
I do believe that the runtime logic is good.
My only concern is about build's complexity, which will make integrating this solution into a 3rd party application pretty difficult.
I believe it's possible to offer the same dispatching capability without the build complexity, but a solution would rely on a capability which doesn't exist yet : combine
XXH_INLINE_ALLwithXXH_NAMESPACE.We could move forward on this proposal in 2 different ways. Either :
I'm happy with any of these. The most important at this stage is to move forward.
edit : I may have underestimated the difficulty with
staticfunctions, however there are other possible solutions with same outcome : generate all possible vector variants, so that they can then be selected at runtime.In branch
multiVersions, there is an unfinished PoC which hopefully exposes clearly enough the proposed design.In this branch, and given proper compilation flags (such as
-mavx512f), the compiler will generate all possible variants (in this case,scalar,sse2,avx2andavx512f), and make them accessible with relevant function name suffix.Then, left alone, it picks only one of them, and rely on the Dead Code Elimination to get rid of the unused ones.
This is achieved without any modification to the build script. Eventually, this should allow a more evolved runtime selection to pick any of them, keeping them all present in the final binary.
edit : in branch
testDispatch2, this PR has been merged withmultiVersions, fixing a few conflicts in the process.Well we still can't rely on that, sadly.
Intrinsics are more complicated than they look, and the compiler can replace intrinsics with better instructions if they are available.
The code you might expect would be this:
But Clang emits this on -msse2:
This on -msse4.1:
And this on -mavx:
Additionally, we need to count autovectorization which can also emit instructions that we don't want.
We could do
__attribute__((__target__))on GCC, but we would have to know whether the GCC/Clang version supports said target, which you can't easily check. It also isn't portable to MSVC and Clang doesn't do a good job at respecting it.As for the build system, 90% of it is feature tests.
It essentially boils down to this:
__attribute__((__target__))is the next logical step.It can be combined with
*_avx2()variants to become*_forceavx2()in the dispatcher.One of the benefits of having a runtime instruction dispatcher in its own unit is that it doesn't have to match the very high level of portability that the library itself must provide.
Which means, I'm fine with a first solution limited to
gcc/clang.That would be more problematic.
FYI, we do use
__attribute__((__target__))onzstd, and haven't met (yet) an issue withclang. So it felt safe. That being said,zstddoesn't have anavx512fcode path either.The debate around build-driven vs source-driven dispatcher generation is partially related to which use case is going to be supported.
If the objective is to make it part of
xxhsum, and limit the "official" support to this one use case,then build-driven is acceptable. We'll have to update the provided build systems, starting with
Makefile, and likely itscmakeequivalent, but hey, that's part of the project, so that's directly manageable. Any further usage is "unsupported".If the objective is to support integration of the dispatcher into 3rd party applications, I believe the build-driven approach will significantly dent its reach. That's where source-driven becomes superior, even if its support is limited to some (common) compilers.
Eventually, if there is an ability to offer both strategies, source-driven for supported compilers, build-driven for all other ones, then it would join the best of both world.
To be fair, I'm comfortable with the idea that the dispatcher is for
xxhsumonly, at least for the time being, and reconsider its scope later on. But I remember some comments stating that they wish to integrate this capability into their own applications, hence I'm not completely sure ifxxhsum-only is a good enough scope.@ -156,2 +156,4 @@#endif/* ==========================================* Dispatcher macrosOne annoyance in this PR is that there is a significant amount of code for the dispatcher inside
xxh3.h.Not that I'm expecting
xxh3.hto remain unmodified, it's clear that it is necessary to add a few hooks,but I was wondering if changes within
xxh3.hcould be reduced, and rather transferred intoxxhash-dispatch.c.For example, they could be defined within
xxhash-dispatch.cbefore includingxxhash.h@ -156,2 +156,4 @@#endif/* ==========================================* Dispatcher macrosWell we could have an
ifndef/defineguard and put everything dispatch in another file, but that option causes issues with mostXXH_INLINE_ALLuses.If you are fine with that, I'm fine with that.
Alternatively, we could have
xxhash.hinclude a different header ifXXH_DISPATCHis defined, but that makes things complicated with build paths and can be ugly when it is installed as a system header.@ -156,2 +156,4 @@#endif/* ==========================================* Dispatcher macrosWell, the objective being to reduce potential impact on normal behavior of
xxhash.h,this outcome wouldn't be fine.
That being said, I'm struggling to understand a scenario where inlining would be impacted.
@ -156,2 +156,4 @@#endif/* ==========================================* Dispatcher macrosUnless you include the special header beforehand, the preinclude method would only use the normal code path with XXH_INLINE_ALL.
The include from xxhash.h adds another header to
/usr/include, which is unwanted.@ -156,2 +156,4 @@#endif/* ==========================================* Dispatcher macrosWell yes that's the idea
@ -156,2 +156,4 @@#endif/* ==========================================* Dispatcher macrosI mean, we could nop the header on other arches.
So basically it could be like this
@ -156,2 +156,4 @@#endif/* ==========================================* Dispatcher macrosYes, exactly
In branch x86dispatch, there is a proof of concept dispatch code, reusing verbatim the detection routine developed by @easyaspi314 in this PR, and coupling it with a dispatch strategy entirely source driven, based on
__attribute__((__target__)), hence limited to a couple of supporting compilers.The interesting points in this strategy :
xxh_x86dispatch.hin the user program, it will swap the supported functions by the dispatch variantslibxxhashproperThe second point is pretty critical, as I'm quite afraid of making the main library more difficult to maintain for the sake of an optional capability.
It doesn't mean that the library is not modified. Clearly it is. But the dispatch logic is fully centralized within
xxh_x86dispatch.c.The main modifications within
xxhash.hupdate some internal functions to accept function pointers as arguments. It makes it possible to swap the specialized implementation ofaccumulate512andscrambleAccwith selectivescalar/sse2/avx2vector implementations from a single main function body. No preprocessor trick is required : inlining function pointers works pretty well !As a side effect, the dynamic dispatch runs the
avx2variant faster than the "regular" library compiled with-mavx2: on my laptop, performance climbs from 50 GB/s to 60 GB/s, quite remarkable, and definitely way above noise level.The PoC is not complete : at this stage, it's only able to replace 2 functions (
XXH3_64bits()andXXH3_64bits_withSeed()). But it's already illustrative of selected design. Supporting more functions will be achieved by applying the same recipe almost mechanically.Also, when compiled with
-mavx512f, something in the detection routine makes the executable fail at runtime :[1] 98930 illegal hardware instructionThis feels like an
avx512instruction is used on a cpu withoutavx512support, which obviously doesn't end well. But it seems to happen insideXXH_featureTest(). Something to look at ...maybe we should add
__attribute__((__target__ ("sse2")))for all non-dispatch functions when compiling with x86 runtime dispatching feature.You can only turn on features with
__attribute__((target)), if you compile with-mavx512f, you are giving the compiler the green light to use AVX512 where it wants.That is the main issue I have with
__attribute__((target)): It can sometimes be unpredictable when not used correctly.You would have to compile for a generic target and have the
__attribute__((target))turn on features.After further tests, and increasing the nb of traces,
I do no longer believe the issue to be located in
XXH_featureTest().Rather, it comes later, after the
avx2variant get selected.That would be an explanation.
I'm trying to shield the variant from the impact of auto-vectorization, by specifying (for
avx2variant) :__attribute__((__target__ ("avx2"))) __attribute__((__target__ ("no-avx512f"))).If the
no-avx512fis not effective, that would explain the issue.A pity that the compiler doesn't produce any warning, it seems to accept the
__attribute__((__target__ ("no-avx512f")))just fine, but if the directive is not effective, it's misleading.Well it "accepts" it and sorta obeys it, but the issue is that
-mavx512fturnsavx512fon for the entire translation unit meaning we would have to turn it off for every other function. It also doesn't play along well with intrinsics from what experienced.It is 100x safer to use
targetto turn on features.Good point.
Let's reverse the logic then.
I'll update the PoC later today to start from a minimal capability, and then selectively enable additional instruction sets.
The
x86dispatchPoC has been updated,employing the new strategy of starting from a baseline (typically
x64+sse2) and adding vector instructions selectively.Everything seems to work fine. No more crash. And all properties mentioned before remain identical.
(It's even a little bit easier to build, as
xxh_x86dispatch.cdoesn't require any vector compilation flag anymore).So I am wondering... Wouldn't it be easier to do the table method?
That means this;
That would abstract away the function pointers, as you can just do
XXH3_dispatchTable->hashLong(...). It also fixes the DRY issue.Additionally, you can just place a macro like
I presume this could be helpful, yes, though the devil is in the details.
In particular, one goal is to preserve
xxh3.has much as possible.I understand that repeating code is instinctively "bad", but keeping in mind that
libxxhashis far more valuable thanx86dispatch, one has to be cautious about the amount of changes thatlibxxhashcan reasonably accommodate for the benefit ofx86dispatch. Copying a few lines of code intox86dispatchis not the end of the world, and improving that situation should not come "at any cost".But sure, this can be improved incrementally. Given it's a PoC, there's a lot to improve.
As an example, I'll likely consider introducing a structure of function pointers as you suggest, as opposed to N function pointers as it is done in the PoC now. This feels better.
In branch x86dispatch_table, there's a new version of
xxh_x86dispatch.c, which adds support for another functionXXH3_64bits_withSecret_dispatch(), and uses a table ofstructin order to group all variants into aconsttable of function pointers.It simplifies the logic a bit, removing several lines of code.
As a side effect though,
XXH_64bits_dispatch()loses some speed in AVX2 runtime, coming back to 50 GB/s (as before). Interestingly,XXH_64bits_withSeed_dispatch()doesn't, so it still runs at ~60 GB/s. This makes it faster than the regular variant !I presume this speed impact is not directly related to the code, but is more likely a side effect of instruction alignment, which is more or less random from pure C (could be tamed with assembly though). Still, it's a surprisingly high impact.
@Cyan4973 did we try
-march=x86-64 -mtune=avx512foptions to generate lean instructions for common functions?-mtunerequires either a CPU name, vendor name, orgeneric.I would consider
-mtune=haswell, ashaswelltunes are pretty generic (likecore2), but it also works around the GCC AVX2 bug.As for what
__attribute__((target("X")))does, it simply simulates that a function is compiled with-mX(with some restrictions).The reason I recommend adding the SSE2 flag is that i386/i686 targets don't necessarily have SSE2.
-march=i686will (unless overridden by the target) compile for a Pentium II which lacks SSE2.The code in branch x86dispatch_table has been updated again, this time reducing the amount of source code dealing with small inputs.
All entry points coalesce into one generic function
XXH_64bits_internal(), employing function pointers to split processing again on reaching the next stage, dealing with long inputs.This reduces code repetition, and in a lesser way code size, as the effect is mitigated by the need to employ generic all-encompassing interfaces, which are then adapted, costing a few additional lines here and there.
I'm now looking for issues like this one : https://github.com/Cyan4973/xxHash/blob/x86dispatch_table/xxh3.h#L1788
where the source code presumes that it is being compiled for a single clear vector instruction set.
Unfortunately, that's no longer the case when using the dispatcher, since any mode, from
scalartoavx512, could end up being selected at runtime.I guess I'll have to settle for something simpler and more generic, even if it ends up being less optimized for a few targets.
That
XXH3_64bits_internalalso has the side effect of reducing code size on-Oz/-Osby a good 100+ bytes (albeit less on thumb)devx86dispatch_tableSuperseded by #387
Pull request closed