Repository navigation
Conversation
ptweezy
marked this pull request as draft
November 8, 2026 01:38
At level 1, ht_matchfinder_longest_match() checks whether each of the two entries in the hash bucket is in the window before comparing the bytes at it. On inputs with few matches, these checks are hard to predict: on random data, level 1 mispredicts about 0.7 branches per byte, nearly twice as many as on dickens. Add ht_matchfinder_longest_match_branchless(), which compares the bytes first and checks the window only if they match. ht_matchfinder_node_ptr() computes an entry's pointer without branching; for an entry out of the window, it points into [in_base, in_next], so it can be read as far as in_next can. On random data, this leaves under 0.005 mispredicted branches per byte. Always checking this way would make level 1 up to about 6% slower on text, and any change to the loop in deflate_compress_fastest() can slow it down through register allocation or code placement. So that function is left as it is, and libdeflate_deflate_compress() uses the new deflate_compress_fastest_adaptive() only for level-1 inputs of at least 16 MiB in which deflate_fastest_few_matches() finds few matches. The probe parses four 4 KiB windows greedily and requires more than 8 literals per match in each. Then, to see matches up to 32 KiB back, it parses the first window again after inserting the 28 KiB before it, and requires more than 2. The text files tested fail in the first window, after 1 to 2 KiB. deflate_compress_fastest_adaptive() has a copy of the loop for each way of checking; after each interval of up to 32 KiB, it uses the new way for the next one if that one had fewer matches than literals. The output is unchanged. Speed change at level 1 on a Ryzen 7 9800X3D (Zen 5), with -O2 -falign-functions=4096, from user-mode cycles (median of 11 interleaved rounds of the best of 3 runs, averaged over two passes): gcc 13 clang 18 Silesia, whole files -0.04% +0.00% enwik8 -0.01% +0.09% Silesia, each file gzipped +160% +220% random data +175% +250% 16 MiB synthetic, few matches +15% to +42% +17% to +51% 16 MiB synthetic, rejected -0.21% to 0% -0.33% to +0.03% No Silesia file was more than 0.34% slower, and an A/A control moved single inputs by up to 0.24%. The synthetic inputs are random bytes over 13 to 16 symbols, random 4- or 8-byte tokens, and random bytes with 85% to 95% of them in short copies. The six that the probe rejects have most of their matches 4 to 32 KiB back, so they reach its second stage: the probe costs the most on them, and less as inputs grow. Tested: identical output at levels 0, 1, 2, 6, 9 and 12 (gcc, and clang with ASan and UBSan); with the minimum at 4 MiB, also at level 1 under qemu (i386, s390x, armhf, aarch64), MSan and valgrind; with it at 256 KiB, the deflate_compress fuzzer (734,441 runs) and a fuzzer comparing the output with the old code's (215,966 runs); and scripts/run_tests.sh with gcc and clang.
ptweezy
force-pushed
the
level1-few-matches
branch
from
November 8, 2026 01:40
1b8cb3d to
0c51e0c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Opening this as a draft to ask whether it's worth having. It makes level 1 much faster on large inputs that barely compress, such as gzipped or random data, and leaves all other inputs on the existing code path. It adds about 420 lines.
At level 1,
ht_matchfinder_longest_match()checks whether each hash table entry is in the window before comparing bytes. On random data, level 1 mispredicts about 0.7 branches per byte, nearly all of them in these checks. This adds a check that compares the bytes first. A copy of the level-1 loop,deflate_compress_fastest_adaptive(), switches to it after any 32 KiB interval with fewer matches than literals. Only level-1 inputs of at least 16 MiB, in which a small probe finds few matches, go there.deflate_compress_fastest()is unchanged, and so is the output.Speed change at level 1 (for the runners, the range of the means per runner type and compiler):
Costs:
deflate_flush_block(). That changed the speed of the unchanged code by -0.7% to +2.1%, depending on compiler and CPU.Measurement details and testing are in the commit message.