Optimize chunker hot loop for ~1.5-2x throughput#307
Merged
Conversation
Replace the two main bottlenecks in the chunker's byte-processing loop: the expensive DIVL instruction for boundary detection (~26 cycles/byte) and the modulo-48 window index (~14 instructions/byte). Key changes: - Replace modulo boundary check with Lemire's fast divisibility test (multiply-and-compare, ~5 cycles vs ~26 for hardware division) - Convert hashTable from slice to [256]uint32 array (eliminates bounds checks) - Add precomputed hashTableRotated table (removes one RotateLeft32 per byte) - Hoist struct fields into local variables in the hot loop - Replace modulo-48 window index with branch (predicted once per 48 iterations) - Reuse backing buffer in fillBuffer() to reduce allocations to O(1) - Add b.SetBytes() to benchmarks for direct MB/s reporting - Modernize benchmarks to use b.Loop() and bytes.NewReader All optimizations preserve identical chunk boundaries, verified by TestChunkerLargeFile which checks exact SHA512/256 hashes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JeremieA
pushed a commit
to InSimo/desync
that referenced
this pull request
Apr 10, 2026
…ht#307) Replace the two main bottlenecks in the chunker's byte-processing loop: the expensive DIVL instruction for boundary detection (~26 cycles/byte) and the modulo-48 window index (~14 instructions/byte). Key changes: - Replace modulo boundary check with Lemire's fast divisibility test (multiply-and-compare, ~5 cycles vs ~26 for hardware division) - Convert hashTable from slice to [256]uint32 array (eliminates bounds checks) - Add precomputed hashTableRotated table (removes one RotateLeft32 per byte) - Hoist struct fields into local variables in the hot loop - Replace modulo-48 window index with branch (predicted once per 48 iterations) - Reuse backing buffer in fillBuffer() to reduce allocations to O(1) - Add b.SetBytes() to benchmarks for direct MB/s reporting - Modernize benchmarks to use b.Loop() and bytes.NewReader All optimizations preserve identical chunk boundaries, verified by TestChunkerLargeFile which checks exact SHA512/256 hashes. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
|
We see huge improvements on our workloads (even slightly more than 2x), thanks a lot! |
Owner
Author
|
Glad it's working well for your use-case. |
Owner
Author
Contributor
|
@folbricht, thanks for sharing that! I'll try to check it out in the upcoming days. |
folbricht
added a commit
that referenced
this pull request
Jul 8, 2026
Restructure the buzhash rolling-hash loop in Chunker.Next: - Drop the ring-buffer window. The window bytes always live in the read buffer, so the byte leaving the window at position pos is simply buf[pos-ChunkerWindowSize]. This removes a store, an index increment and a wrap branch per byte, along with the hValue/hWindow/ hIdx fields whose state never survived across chunks. - Replace the two-part divisibility test with a branch-free multiply-and-rotate form (Hacker's Delight 10-17): hValue % d == d-1 is evaluated as "hValue+1 divisible by d" via rotr32((hValue+1)*inv(odd), k) - qBias <= qMax. This avoids Go's variable-shift overflow guard and reduces register pressure that previously spilled two constants to the stack in the hot loop. - Unroll the loop 2-way using h2 = rol2(h) ^ rol1(a0) ^ a1, halving the loop-carried rotate/xor dependency chain. The new boundary test also fixes a subtle bug introduced in #307: the old test computed hValue - (d-1), which wraps around for hValue < d-1, and since 2^32 is not divisible by d, exactly one hash value per discriminator (14413 for the default 64KB average chunk size) declared a false chunk boundary compared to the plain modulo. The new form was verified exhaustively against "hValue % d == d-1" over all 2^32 hash values for a range of discriminators, including powers of two, and TestChunkerBoundaryTest pins the equivalence for the wrap-around edge cases. Benchmarks (i7-7600U, interleaved runs against master): BenchmarkChunker ~369 MB/s -> ~499 MB/s (+35%) BenchmarkChunkNull50M ~255 MB/s -> ~433 MB/s (+70%) desync make -n 1 512MB 2.74s -> 2.27s Index output is byte-identical to master on a 512MB random test file.
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.
Summary
DIVLinstruction in boundary detection (hValue % disc == disc-1) with Lemire's fast divisibility test — a multiply-and-compare that costs ~5 cycles vs ~26 for hardware divisionhashTablefrom slice to[256]uint32fixed-size array, eliminating bounds checks on all accesseshashTableRotatedtable, removing oneRotateLeft32call per byte in the hot loophValue,hIdx,buf, window pointer, Lemire constants) into local variables to avoid repeated pointer dereferences(hIdx + 1) % 48with a branch (if hIdx >= 48) — perfectly predicted, taken once per 48 iterationsfillBuffer()to reduce allocations from O(filesize/buffersize) to O(1)b.SetBytes()to benchmarks for direct MB/s throughput reporting; modernize to useb.Loop()All optimizations preserve identical chunk boundaries, verified by
TestChunkerLargeFilewhich checks exact SHA512/256 hashes for every chunk.Test plan
go test -run TestChunkerLargeFile— exact chunk boundary regression (SHA512/256 hashes)go test -run TestChunker— all chunker tests passgo test -bench=BenchmarkChunk -benchmem— verify throughput improvement🤖 Generated with Claude Code
Before:
After:
Closes #244