WIP: Refactor to improve code size, fix GCC 12 #802
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 assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-Cyan4973/xxHash!802
Loading…
Reference in a new issue
No description provided.
Delete branch "streaming-refactor"
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?
Overview
Kinda a dump of (mostly) related changes I've been sitting on and never bothered to commit.
This is a set of changes to:
-O3-O3could get well over 100 kB.texthashLongconstant propagation can be reduced-Os/XXH_SIZE_OPTperformanceThese haven't been micro-benchmarked yet. The "what is worth inlining" is a rough estimate, and I want to go through the various recent compilers to find what works best.
Major changes:
-O3to work around the-Ogbug. Fixes #800, #720__OPTIMIZE__,!__NO_INLINE__andXXH_SIZE_OPT <= 0(!__OPTIMIZE_SIZE__), so-O2and-Ogboth applyXXH_INLINE: Acts like existingXXH_FORCE_INLINEwhere it is disabled whenXXH_SIZE_OPTis used.XXH_FORCE_INLINEnow actually force inlines unconditionally (when inline hints are on) and is used on XXH3 and XXH32's core loops and utilities.-Osperformance with only a small code size overheadXXH_32BIT_OPT,XXH_INLINE_64BIT: These are used to reduce code bloat on 32-bitclang armv4t -O3code size by about half, now being <100 kBXXH32andXXH64have been rewritten to reuse the algorithm stepsXXH3_updatehas been rewritten to be much easier to understand and much smaller.XXH3_128bits_updatenow tail callsXXH3_64bits_update, greatly reducing code size at the cost of one branchMinor changes:
-O2on AVX2, that bug has been fixed.XXH32_state_tandXXH64_state_thave some fields renamed and now useunsigned char [N]for their bufferXXH_OLD_NAMEShas been removed, it's been long enoughSeems like some apt repos are down which is causing some things to fail.
I also have an idea that seems to get full performance (at least on large scale, I'll have to test the latency) with only 2 copies of
XXH3_hashLong_internal_loop.This manages to get
clang aarch64 -O3to 24 kB compared to 36 kB, andclang armv4t -O3from 92 kB to 61 kB (far less than half of the 160 kB before this PR)Returning a struct like this is functionally identical to memcpying to an out pointer on the ABI level, but the compiler knows more about it and can optimize it better. (Just having an output pointer without an inlining is 40% slower).
I would have to clean it up, rename it, and test it some more though, so I would probably keep this to another PR.
Edit: This could be a step towards making dispatching on x86 standard as well since the overridden functions are now only 2-3 (also the feature test is 99% logging - it normally compiles to like 50 instructions).
How do the rest of these changes look aside from that (which since it would affect dispatching is a huge change that will need some more testing)? The changes shouldn't affect 64-bit codegen that much (although XXH3_update may be worth some benchmarking)
The biggest changes codegen-wise are 32-bit not being forced to inline and unroll ugly functions as much (which have more latency than a call frame/loop and aren't worth the code size increase) and
-Osbeing faster.I'm going to leave the hashLong rework to another PR because that would need reworking dispatching, and that would be a perfect time to redo that mess.
I'm trying to get a sense of the scope of the changes in this PR.
To rephrase it :
-Oscompilation mode, performance modes-O2and-O3should not be negatively impacted-Oswhere it's expected to be positive.armtarget is differently impacted thanx86/x64one.If that's the correct scope, then there are a number of measurements to do and publish to document this PR.
Longer term, I wonder if there is something that could / should be done to ensure these properties are preserved in the future. Otherwise, it would be easy for a future PR to inflate the binary size without anyone noticing it.
I made a series of measurements, leading to surprising results (at least for me), when comparing this
pr802todevbranch.Let's start with
x64compilation mode. Compiler isgccv9.4.0, cpu isi7-9700k(turbo off). Performance flag only, meaning the vector extension is defaultSSE2. Let's measure the size of producedlibxxhash.devlib sizepr802lib size-O3-O2-OsSo that's the first surprise : the changes are beneficial to binary sizes for the performance mode, but when it comes to the size optimized
-Osmode, binary size is actually increased. It's not necessarily a bad deal, but since it seems to achieve the inverse of what's advertised, I figured it's worth underlining.Now let's have a look to performance. The scenario measured is the default
-b, aka 100 KB (long hash). Numbers measureXXH32,XXH64andXXH128successivelydevGB/spr802GB/s-O3-O2-OsXXH128is improved, even faster than-O2To begin with, at
-O3and-O2,pr802features no speed change. So, combined with the lower binary size, thispr802is a win globally. Nicely done.Note how performance of
XXH128plummets seriously at-O2, which is probably becausegccneeds-O3to generate efficient vector code.XXH32andXXH64in contrast remain unaffected.Finely, at
-Os,XXH32andXXH64remain performance neutral, whileXXH128improves withpr802, to the point of running slightly faster than-O2(yet far from-O3). I wonder if there are some useful learning there.That makes
-Osplainly better than-O2onpr802, since it wins on both speed and binary size.So, some surprises, but all in all, this PR has positives.
One could still argue that, for
XXH32andXXH64,dev+-Osis actually the better choice, since performance is unaffected, while binary size is the smallest.But for
XXH128, note that the larger binary size of-Oscomes with better performance too, so it's a reasonable trade off. In any case,pr802+-Osis superior in all metrics todev+-O2, since it provides both smaller binary size and faster speed forXXH128. That's worth mentioning.Now, it was also mentioned that this PR is supposed to be even more useful for 32-bit mode, so let's try that. Once again, compiler is
gccv9.4.0, only performance flags are changed, meaning that vector extensions are likely disabled by default.devlib sizepr802lib size-m32 -O3-m32 -O2-m32 -OsThe first thing to note is how big these binary sizes are, compared to
x64binaries. This might be because there is no vector extension by default. This could be tested in more details by separatingXXH3variants from the rest.The binary size decrease from
pr802is huge for the performance modes, especially at-O3.Once again,
-Osis against the trend, though the difference is pretty minimal in this case, that's essentially a tie.Now, what about performance ?
devGB/spr802GB/s-m32 -O3-m32 -O2-m32 -OsOnce again,
xxh32proves very resilient, offering the same performance at all compilation modes, and note that it's the same performance as thex64mode, despite the lower nb of registers.We already knew that
xxh64is a bad fit for 32-bit mode, and this is confirmed again in these measurements, no surprise. Note though that in this case,-O3does deliver some speed benefits (almost +10%), so it's no longer clear that-Osis the better choice forXXH64.XXH128performance is also much lower, most likely due to the disappearance of vector extensions. IfSSE2was still there, it would be a very different story. Anyway, in this case,-O3improves the performance ofXXH128by a whopping +100%, so it really matters.Finally, in these measurements, performance of
pr802seems slightly lower thandev. Nothing extravagant, but worth mentioning.It's difficult to draw conclusions for this compilation exercise.
pr802delivers significant size benefits for-O3and-O2, and the minor performance losses are probably acceptable. What's more concerning is the-Osmode, for whichpr802is both slightly larger, and slightly slower. Nothing "serious", but still not in the right direction. And I initially expected this mode to be the most beneficial forpr802, so clearly, there was some misunderstanding.There are other axis that could be analyzed. For example, add an
aarch64test. And maybe a 32-bitarmv7too. Measure the binary size impact ofXXH3separately from legacyXXH32andXXH64. Add a few different vectors. Add a few different compilers. The test matrix quickly becomes overwhelming, and it's likely going to be difficult to produce a change that's only beneficial.One potential idea : is it possible to break this PR into smaller stages ? Maybe some of these stages produce uncontroversial benefits that could be quickly merged ? Maybe other require pondering some trade-offs (which can still be acceptable) ? Finally, maybe there are useful learnings that could be used to improve other parts of the PR ?
Open for discussion.
Also, I would recommend rebasing this PR on top of
dev, just to make sure that observed performance differences come solely from this PR, and are not confused with some parallel PR that would have been merged todevbut notpr802.Since the focus of this PR is on binary size, I wanted to have a more in-depth look at these evolutions.
In the following tables, I'm separating the variants
XXH32,XXH64andXXH128, by producing limited dynamic library binaries (note: maybe I should have used static library size instead).For
XXH32, the producedlibxxhashonly containsXXH32and some common functions.For
XXH64, the producedlibxxhashhas both previous content andXXH64, but noXXH3variant.Finally, for
XXH128, it's the full library.The hope is that it will make it possible to observe the binary size changes of
XXH32andXXH64separately, without mixing the (probably larger) changes due to vector extensions exploited byXXH3variants.In the same vein, it's going to be more natural to add more vector extensions for comparisons.
Let's start this exercise with
XXH32:devsizepr802sizex64gccv9.4.0-O3x64gccv9.4.0-O2x64gccv9.4.0-Osx86gccv9.4.0-m32 -O3x86gccv9.4.0-m32 -O2x86gccv9.4.0-m32 -Osaarch64M1clangv14.0-O3aarch64M1clangv14.0-O2aarch64M1clangv14.0-OsA few interesting details here.
To begin with, we know that performance remains largely unaffected for
XXH32, staying at6.9 GB/son thei7-9700kno-turbo target cpu, whatever the compilation mode and flags. The story is essentially the same onM1, withXXH32delivering consistently7.0 GB/s, and small (unimportant) degradation atpr802with-Ossetting.We also note that the library size differences between modes and commits remain extremely small, which is consistent.
Two remarquable trends are worth mentioning though :
pr802. At the end of the story, the differences are not large, but that still goes against expectations.pr802is especially worse at binary size for the-Osmode. This is also the reverse of my initial expectations.It could be that binary size differences are in fact unrelated to
XXH32proper, and due to minor differences in other generic symbols still compiled in the library, such asXXH_versionNumber()for example.In conclusion, when it comes to
XXH32, thispr802is roughly neutral.Now, onto
XXH64:devsizepr802sizex64gccv9.4.0-O3x64gccv9.4.0-O2x64gccv9.4.0-Osx86gccv9.4.0-m32 -O3x86gccv9.4.0-m32 -O2x86gccv9.4.0-m32 -Osaarch64M1clangv14.0-O3aarch64M1clangv14.0-O2aarch64M1clangv14.0-OsSome learnings:
In
x64mode, the library size difference is very small compared to theXXH32-only library. Less than ~0.5KB is added. This is a similar story foraarch64. This seems to show thatXXH64is compact enough, at all compilation settings.As we already know, speed remains unaffected, at
13.7 GB/soni7-9700k. It is only slightly degraded at-Oswithpr802onM1 Pro, still offering roughly doubleXXH32speed.Unfortunately,
pr802doesn't offer any binary size savings. Actually,-Osis slightly bigger thandev.On
x8632-bit mode, there is a more substantial binary size difference, anddevbranch requires > +4 KB to addXXH64. This suggests that theXXH64algorithm gets expanded significantly. Probably some loop unrolling.Binary size is improved by
pr802, showing that it's possible to addXXH64with a budget of roughly ~0.5KB, similar tox64mode.We also know that
XXH64performance is much lower in 32-bit mode, due to reliance on 64-bit multiply operations.And unfortunately,
pr802makes this situation slightly worse.This would probably remain an acceptable trade-off when it saves almost ~4 KB of binary size, but strangely, at
-O3, there is no such binary size saving, yet the performance difference is still very much present (checked several times).It's unclear to me why there is a (small) performance difference between
devandpr802in 32-bitx86mode.It's also unclear to me why binary size savings seem ineffective at
-O3specifically, while-O2seems to benefit from it.In conclusion, when it comes to
XXH64, thispr802is slightly negative for performance. There is an interesting binary size saving onx86at-O2and-Ossettings, that could potentially compensate the small speed degradation. But for64-bitmodes, there is no positive to report.XXH128:For this last one, we can simply re-employ results from previous measurement exercise.
devsizepr802sizex64+SSE2gccv9.4.0-O3x64+SSE2gccv9.4.0-O2x64+SSE2gccv9.4.0-Osx86+scalargccv9.4.0-m32 -O3x86+scalargccv9.4.0-m32 -O2x86+scalargccv9.4.0-m32 -Osaarch64M1 +NEONclangv14.0-O3aarch64M1 +NEONclangv14.0-O2aarch64M1 +NEONclangv14.0-OsTo begin with, it's clear that library size increases significantly with
XXH3variants. Sure, there are 2 variants, divided into a number of range strategies, plus streaming capabilities, ok. Yet, I'm nonetheless surprised that it requires so much. The-Oscompilation mode is the only one that seems in the "acceptable" range.Here,
pr802brings pretty big binary size savings, at-O2and especially-O3.For
-Osthough, it doesn't. It actually increases size for thex64mode.-O3remains an important compilation flag for best performance ofXXH128ongcc: speed drops very significantly with-O2and-Os.On
clang, the picture is slightly different : speed is exactly the same at-O3and-O2. But at-Os, the performance drop is very steep.The performance impact of
pr802is all over the place, from neutral, to slightly negative, to positive.The most interesting aspect is seeing
-Osmode receiving a speed boost ongcc+ intel cpu.For
x64, this speed boost is significant enough to beat-O2, which is really nice since binary is also smaller.For
x86, the speed boost is not as important, but still a hefty +10% compared todev.So there's probably something worth investigating there.
To be continued : add more targets, more compilers, etc.
The previous analysis was using Dynamic Library to compare generates binary sizes. It was assumed that this wouldn't make much difference compared to Static Libraries, with maybe just a constant factor added.
This is incorrect : Static and Dynamic Library sizes are loosely correlated, but can evolve somewhat differently.
This makes it difficult to decide which one is worth monitoring.
It could be argued that Static Library is closer to the concept of "generated binary size", so that would justify prefering Static library sizes. But this disregards the issue that these fragments of code, once integrated into a real executable (library or program) have to respect additional rules, such as code and data alignment. As a consequence, the impact of a change will be different depending on the target, and depending on the linking process. Therefore, it could also be argued that the Dynamic Library size gives a better sense of the final impact of a change.
This is relatively difficult to make sense of, and difficult to decide, so I left both analysis in the thread. We can continue from the Static or the Dynamic library size analysis, depending on preference.
Since the focus of this PR is on binary size, I wanted to have a more in-depth look at these evolutions.
In the following tables, I'm separating the variants
XXH32,XXH64andXXH128, by producing limited Static library binaries.For
XXH32, the producedlibxxhashonly containsXXH32and some common functions.For
XXH64, the producedlibxxhashhas both previous content andXXH64, but noXXH3variant.Finally, for
XXH128, it's the full library.The hope is that it will make it possible to observe the binary size changes of
XXH32andXXH64separately, without mixing the (probably larger) changes due to vector extensions exploited byXXH3variants.In the same vein, it's going to be more natural to add more vector extensions for comparisons.
Let's start this exercise with
XXH32:devsizepr802sizex64gccv9.4.0-O3x64gccv9.4.0-O2x64gccv9.4.0-Osx86gccv9.4.0-m32 -O3x86gccv9.4.0-m32 -O2x86gccv9.4.0-m32 -Osaarch64M1clangv14.0-O3aarch64M1clangv14.0-O2aarch64M1clangv14.0-OsWe know that
XXH32speed remains largely unaffected, staying at6.9 GB/son thei7-9700kno-turbo target cpu, whatever the compilation mode and flags. The story is essentially the same onM1, withXXH32delivering consistently7.0 GB/s, and a small (unimportant) degradation atpr802with-Ossetting.The library size differences between modes and commits remain small, which is unsurprising.
But we can also notice that
pr802impact is generally negative for binary size.Not by a large amount, therefore it's not a big deal.
Since there is no corresponding speed benefit,
pr802looks globally slightly negative for theXXH32-only scenario.But as mentioned, this is minor, and can probably be ignored if there is a much larger benefit for other scenarios.
Now, onto
XXH64:devsizepr802sizex64gccv9.4.0-O3x64gccv9.4.0-O2x64gccv9.4.0-Osx86gccv9.4.0-m32 -O3x86gccv9.4.0-m32 -O2x86gccv9.4.0-m32 -Osaarch64M1clangv14.0-O3aarch64M1clangv14.0-O2aarch64M1clangv14.0-OsSome learnings:
As we already know, in 64-bit modes (
x64,aarch64), speed remains unaffected, at13.7 GB/soni7-9700k. It is only slightly degraded at-Oswithpr802onM1 Pro, still offering roughly doubleXXH32speed.Unfortunately,
pr802doesn't offer any binary size savings for this scenario. It is actually slightly bigger thandev.On
x8632-bit mode, there is a more substantial binary size difference, anddevbranch requires 2x binary size budget to addXXH64. This suggests that theXXH64algorithm gets expanded significantly. Probably some loop unrolling.Binary size is improved by
pr802, showing that it's possible to addXXH64with a budget roughly similar tox64mode.We also know that
XXH64performance is much lower in 32-bit mode, due to reliance on 64-bit multiply operations.And unfortunately,
pr802makes this situation slightly worse.This would probably remain an acceptable trade-off when it saves binary size, but strangely, at
-O3, binary size savings are not great, yet the performance difference is very much present (checked multiple times).It's unclear to me why there is a (small) performance difference between
devandpr802in 32-bitx86mode.It's also unclear to me why binary size savings is less effective at
-O3specifically, while-O2seems to benefit more from it.In conclusion, when it comes to
XXH64, thispr802seems slightly negative for performance. There is an interesting binary size saving onx86, especially at-O2and-Ossettings, that could potentially compensate the small speed degradation. But for64-bitmodes, there is unfortunately no positive outcome to report .XXH128:Let's redo binary size measurements, using
staticlibrary size instead.devsizepr802sizex64+SSE2gccv9.4.0-O3x64+SSE2gccv9.4.0-O2x64+SSE2gccv9.4.0-Osx86+scalargccv9.4.0-m32 -O3x86+scalargccv9.4.0-m32 -O2x86+scalargccv9.4.0-m32 -Osaarch64M1 +NEONclangv14.0-O3aarch64M1 +NEONclangv14.0-O2aarch64M1 +NEONclangv14.0-OsTo begin with, it's clear that library size increases significantly with
XXH3variants. Sure, there are 2 variants, divided into a number of range strategies, plus streaming capabilities, ok. Yet, I'm nonetheless surprised that it requires so much. The-Oscompilation mode is the only one that lies in the "acceptable" range.Here,
pr802brings pretty big binary size savings, at-O2and especially-O3.For
-Osthough, it's less clear, and can actually increase size.-O3remains an important compilation flag for best performance ofXXH128ongcc: speed drops very significantly with-O2and-Os.On
clang, the picture is slightly different : speed is exactly the same at-O3and-O2. But at-Os, the performance drop is very steep.The performance impact of
pr802is all over the place, from neutral, to slightly negative, to positive.The most interesting aspect is seeing
-Osmode receiving a speed boost ongcc+ intel cpu.For
x64, this speed boost is significant enough to beat-O2, which is really nice since binary size is also smaller.For
x86, the speed boost is not as important, but still a hefty +10% compared todev.So there's probably something worth investigating there.
Note : some results are sometimes surprising. Ensuring that
pr802is based on top ofdevis also important to ensure this comparison analysis doesn't produce wrong conclusions.To be continued : add more targets, more compilers, etc.
Sure, I can do that. Sorry I adhd'd again 😵💫
I think that the isolated changes that are not going to be controversial are:
-Os. However, perhaps don't do it for anything aboveroundoracc512, and don't use noinline either.The future changes could be
a. Implement the hashLong refactor
b. Improve the 4 and 5 selections on testing
c. If we are going to be reducing inlining, perhaps we could sneak in a
__attribute__((regparm(3)))on x86 to make tail calls better?Yes. That is expected.
On 64-bit targets, the size decrease on
-O3comes fromXXH3_update, which has been refactored and is no longer inlined twice. There haven't been that many changes otherwise.The size increase on
-Osis expected too, as I have changed it to inline the utilities and core functions. However, 4kB is a little more than I want — perhaps that comes from theXXH_NO_INLINEclones.This change is because
-Osis a pretty common default optimization level in various projects, and the aggressive size optimizations should be more opt-in. This is actually the reason Clang added-Oz,-Oswas originally an alias for-O2because people liked to overuse it.The performance issues were noticeable more on GCC ARMv4, where it outlined
XXH_memcpy()for unaligned reads.Although I will have to look into fixing the GCC x86 performance issues. This might just be from the
XXH_INLINE_64BITnot inlining the right functions since the performance drop seems to be consistent.The way it is force inlined makes there be 8.
You can easily see the size of each function if you compile with
-ffunction-sections, and usesize -A xxhash.o | sort -nk2.The short hashing code (XXH3_[64,128]bits_internal) is deceptively heavy on 32-bit. When compiled with
XXH_NO_INLINE_HINTS, it is usually outlined and can be 1-2k itself on 64-bit targets, and 5-10k on 32-bit.However, arguably, there is some benefit to this, as the short hash is more sensitive to latency, and the constant propagation of
kSecretandseed==0can be beneficial (especially on platforms with limited unaligned access since all secret reads are aligned).Additionally, as I mentioned before, the way hashLong is set up causes inline clones as well, which afaik doesn't seem to be beneficial now that the codepaths are identical.
Another note is that on x86, the 64->128-bit multiply is a lot of instructions because of how disruptive the
EAX:EDXrequirements are. Both GCC and Clang seem to be unaware of this code weight and inline/unroll 60+ bytes of code each multiply. (I might make an XXH_NO_INLINE for all 32-bit but ARM with DSP) Also, GCC 10+ on x86 emit a literalimul reg, reg, 0which means I might need to hack mult32to64 on that too.The first thing I'll do is find the minimum amount of
#pragma GCC optimize("-O3")or__attribute__((optimize("-O3")))to shut up GCCI have a rough draft of the hashLong refactor that is designed to be dispatched, but
update()performance is pretty bad due to the function call latency (since consumeStripes() would need a__attribute__((target))). I might have to extract the entire second half ofXXH3_update(). Changes start at ~line 5000.https://gist.github.com/easyaspi314/795084ca1c04d4c884e6cff14cab80a4
I believe there were already several fixes merged for the
gcc-12 -Ogissue.Therefore, is this PR still useful ? Or can it be closed ?
The "fix
gcc-12 -Ogcompilation" issue seems fixed now, thanks to other contributions.However, the PR also lists "improve code size" as an objective.
Is there still something worth extracting from it ?
I compared dev (
258351b) and this PR (98be0b9)Note that this report contains all XXH algorithms.
We can still see some size difference in x86 mode.
CSV
Procedure
As for code size, c6dc92f also reduces it and may implement more robust dispatch functionality.
I should go back and finish that. I kinda got distracted by another project 😅
Ideally, I would like to publish release
v0.8.2in the very near future.I presume completing this objective is not trivial, and therefore should rather be expected as a potential contribution to a later version ? (either
v0.8.3orv0.9.0)Looking over the code I think it just needs some clean up, documentation, and benchmarking.
This PR has remained inactive in "draft" mode for a long period of time.
It seems it attempted something interesting, related to code modularity and reduced binary size, both of which could be valuable.
However, since the underlying source code has continued to evolve since, this PR in now in conflict with the
devbranch.The real question if is this is abandoned, in which case, it would be appropriate to close it.
I've merged a few cherry-picked commits from this PR.
A few others had already been extracted and merged previously.
Some potentially interesting yet missing ones are :
In all cases, the code has diverged enough to make these commits non applicable.
The first 2 ones look interesting, but their impact should be better assessed before proceeding.
The last one makes more sense once combined with previous 2 ones.
That's the most I can extract from this PR at this point, so it will be closed.
Further investigations will likely require a new PR.
Pull request closed