Repository navigation
Speed up the compressor without changing a single output byte - #4
Merged
Merged
Conversation
Every change below was gated on a 589-case corpus of compressed-output SHA-256 hashes staying identical to the straight port, covering all dictionary sizes, both modes, block-boundary payload sizes, and an every-length sweep across the first block boundary. Match measurement is now word-at-a-time. The XOR of two 64-bit loads is zero only when all eight bytes agree, and the trailing-zero count of a non-zero XOR locates the first mismatch; on x86-64 this JITs to unaligned 64-bit loads and TZCNT. This is sound because the rewrite preserves the original's exact stopping rule, including the quirk of counting the second byte unverified: within a hash chain the pair hash b0*4 + b1*5 is equal, so equal first bytes force equal second bytes. The pointer-advancing distance arithmetic collapses to start-of-match form, with the original's end-of-loop distance decrement folded away. An AVX2 32-byte loop was measured and lost; matches are capped at 0x204 bytes and usually far shorter, so wider vectors cost more in setup than they saved. The candidate-scan prechecks fold two byte comparisons into one branchless test, (a ^ b) | (c ^ d) == 0, turning two poorly predicted branches into one rarely taken one. On small-alphabet data, where half the candidates match on the first byte, this alone was worth roughly half again as much throughput. The chain-walk loops use unchecked reads via Unsafe/MemoryMarshal, with the bounds argument documented at each site: every chain is terminated by the entry for the input position itself, and stored offsets sit at least MaxRepetitionLength + 4 bytes inside the padded work buffer. The bit writer is unrolled into a loop with its cursor in locals in place of the original's recursion, preserving the deliberately unmasked spill byte. No unsafe blocks; AllowUnsafeBlocks stays off. Median throughput, 4 MB payloads, binary mode, 4096-byte dictionary: repetitive text 190 -> 310 MB/s mixed runs and noise 105 -> 109 MB/s random bytes 46 -> 47 MB/s 8-symbol alphabet 7 -> 11 MB/s The small-alphabet case is slow by nature: the format's hash chains degenerate there and honoring the original's match selection means walking them in full. Decompression is untouched. All 966 tests pass, plus a 784-check independent harness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Speeds up
ImplodeEngineunder one hard constraint: the output may not change by a single byte. Every optimization was gated on a 589-case corpus of compressed-output SHA-256 hashes (all dictionary sizes × both modes × payloads including block-boundary sizes and an every-length sweep from 1 to 1600) staying identical to the straight port. Decompression is untouched.Results
Median throughput, 4 MB payloads, binary mode, x86-64 desktop (±3% run-to-run):
What changed
1. Word-at-a-time match measurement. The byte-at-a-time compare loops in
FindRepetitionandExtendRepetitionare replaced by a helper that XORs two 64-bit loads — zero only when all eight bytes agree — and locates the first mismatch with the trailing-zero count. On x86-64 this JITs to unaligned 64-bit loads plusTZCNT.This rewrite is only safe because of a property worth writing down: the original counts the second byte of a candidate as matching without comparing it. That is sound, not a bug — within one hash chain the pair hash
b0*4 + b1*5is equal, so equal first bytes force equal second bytes in exact integer arithmetic. The distance bookkeeping (the original computes it from advanced pointers, then decrements once in the max-length case) algebraically collapses toinput − matchStart − 1in every path, which is what the new code stores directly.Intrinsics that lost: a 32-byte AVX2 compare loop was also measured, per the request — it was slower. Matches are capped at 0x204 bytes and typically far shorter, so the wider vectors cost more in setup than they saved. The 64-bit/
TZCNTform won and is what ships.2. Branchless prechecks (the worst-case win). The candidate scan tested two bytes with
&&— two hard-to-predict branches on small-alphabet data, where ~half the candidates match on the first byte. Folded into one branchless test,((a ^ b) | (c ^ d)) == 0, the surviving branch is rarely taken and predicts well: +54% on the degenerate case by itself.3. Unchecked reads in the chain walks, via
Unsafe/MemoryMarshal, with the safety argument documented at each site: every chain is terminated by the entry for the input position itself (so the index cannot leave the table), and every stored offset sits at leastMaxRepetitionLength + 4bytes inside the padded work buffer. Nounsafeblocks;AllowUnsafeBlocksstays off.4. Iterative bit writer.
OutputBitsrecursion unrolled into a loop with the cursor in locals, preserving the original's deliberately-unmasked spill byte (the two-round overlap where the same bits are OR-ed over themselves).Verification
dotnet test: 966 passeddotnet build -warnaserror: cleanREADME gains a Performance section, and the "no unsafe code" line now states precisely what is and isn't used.