Repository navigation
zstd, huff0: cheaper per-block table setup - #1234
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughThe changes update Huffman weight parsing and decoder-table filling. They also change Zstandard FSE decoding to construct table entries from predefined symbol-extension data across generic and assembly implementations. ChangesHuffman table construction
Zstandard FSE decoder tables
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~30 minutes Change: Refactor Merge Risk: ⚪ Minimal · up to No actionable merge-blocking risk is established; the PR is mergeable after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The changed decoder appears to reject out-of-range table symbols before using them, and no introduced security issue was established. Some error-recovery behavior remains unverified. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The sequence decoder built each FSE table in two passes: buildDtable spread the symbols and set nbBits/newState, then transform rewrote every state again to replace the symbol with its addBits and baseline, checking the symbol bound on each entry. readNCount already limits symbols to the table's range, so that check could not fail. Precompute each table's per-symbol addBits/baseline once, and have the build loop merge them into the entry it stores, as the reference decoder does. The asm build loop now does one 8-byte store per state instead of a byte and a word store. maxBits is the last symbol's addBits, since addBits never decreases with the symbol in any of the three tables; initPredefined checks that. BenchmarkBuildDtable now includes the work transform did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Weights repeat, so counting them into a single histogram made each increment wait on the previous store to the same counter. Count into four histograms and merge; the weight total and the too-large check are taken from the merged counts. Fill runs of 4 or more entries 4 at a time. TestReadTableWeights (decompress_test.go) covers the weight checks inside and after the four-at-a-time loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6920091 to
f20c7fd
Compare
readNCount's main loop stops at maxSymbol, but a run of zero counts only checks against 255, so a table description could skip past the last valid symbol and give the next one a count; the check after the loop allowed up to 256 symbols. transform used to reject such tables. Without it they got states with zero addBits and baseline, and a maxBits that was too low for the refill paths. Check symbolLen against the table's own maximum. TestReadNCountSymbolPastTable in seqdec_test.go gives an offsets table a count for symbol 40 after a zero run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Nice find! |
seqdec_test.go: both sides appended tests at the end; keep both. The regenerated assembly is byte-identical to the merged files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-fold # Conflicts: # zstd/seqdec_test.go
Two independent changes to the per-block table setup, one commit each. Profiling against libzstd (dev
d9c0c7e) on the real corpora from https://klauspost.com/files/compress/ showed per-block table setup costing Go almost twice what it costs C on arm64. On Graviton5,apache.logre-encoded withzstd -3spent 15.1% of Go'sDecodeAlltime building tables versus 9.6% of libzstd's, about 8 of the 13 points Go trailed by on that file.Independent of #1212, #1230, #1232 and #1233: it touches only table construction, is based on master, and applies cleanly on top of all four.
(1) Fold the FSE symbol transform into the table build (fe03c18)
Each sequence table was built in two passes:
buildDtablespread the symbols and set nbBits/newState.transformthen rewrote every state to replace the symbol with its addBits and baseline, checking the symbol bound on each entry. That check was also the only thing rejecting a table that gives a count to a symbol past its range; (3) moves it intoreadNCount.Each table's per-symbol addBits/baseline is now precomputed once in
initPredefined, and the build loop merges them into the entry it stores, as the reference decoder does inZSTD_buildFSETable. The asm build loop (_generate/gen_fse.go, regenerated for amd64 and arm64) now does one 8-byte store per state instead of a byte store and a word store.maxBitsis taken from the last symbol's addBits, because addBits never decreases with the symbol in any of the three tables;initPredefinedpanics if that ever stops holding. For every tablereadNCountaccepts (with (3), no symbol past the table's range), the value is exact, since the last symbol always has a nonzero count. In any other case it can only be too large, which costs an extra bitreader fill but never loses bits.transformwas 3.2–3.4% ofDecodeAlltime on Graviton5 (1.2% on Zen 3) and is gone. The merged store costs nothing measurable:buildDtable_asm's share is unchanged, and it is now at about the same share asZSTD_buildFSETable(6.7% vs 6.35%).BenchmarkBuildDtablenow includes the worktransformdid, so benchstat against master reads +21–43%. Compared like for like (master'sbuildDtable+transformvs thisbuildDtable, Zen 3, n=10): litLength −19.8%, offset −16.0%, matchLength −19.0%, all p=0.000.(2) huff0: faster
ReadTableweight stats and table fill (f20c7fd)ReadTable. Weights are now counted into four histograms and merged. The weight total and the "weight too large" check are taken from the merged counts, with the same errors as before.Test:
TestReadTableWeightsindecompress_test.gocovers the weight checks inside and after the four-at-a-time loop, plus a valid table.The remaining piece of
ReadTableis decoding the weights through the genericfse.Decompress, which is not changed here.(3) Reject FSE counts for symbols past the table's range (d1dbb1a)
readNCount's main loop stops at the table's last symbol, but a run of zero counts only checks against 255. A table description could therefore skip past the last valid symbol and give the next one a count, and the check after the loop allowed up to 256 symbols. On mastertransformrejected such tables; with (1) alone they would get states with zero addBits and baseline, and amaxBitstoo low for the refill paths.readNCountnow checks the symbol count against the table's own maximum. Test:TestReadNCountSymbolPastTableinseqdec_test.go, an offsets table with a count for symbol 40 after a zero run; it fails without the fix.Results
Both commits together, measured single-threaded with checksums ignored. For each A/B, every asm
TEXTsymbol was pinned to 64 bytes in both builds (measurement only), and runs alternated between the two builds. Baseline A is master plus #1212, #1230, #1232 and #1233; B adds this PR. Measured against master alone, the percentages will be slightly larger.Corpora:
DecodeAll: 200 MB slices of apache/gob/json/nyc-taxi, each re-encoded withzstd -3,zstd -19and this package atSpeedDefault, plus thebenchdecoder.zipfiles.apache.logandcockroach.node1.logas provided,Readerwith concurrency 1.Change in time, B vs A (negative is faster):
DecodeAll,-3slicesDecodeAll,-19slicesDecodeAll,SpeedDefaultslicesDecodeAll, small filesBenchmarkDecompress4XTable-3−4.1%, gob-3−3.6%, json-3−3.5%, comp-data.bin −7.1%, geo.protodata −4.7%, html −4.5% (all significant).DecodeAllcells are faster and significant, from −0.35% to −6.2%.BenchmarkDecoder_DecodeAll: 12 of 12 files faster on Graviton5, and 11 of 12 on Sapphire Rapids (fireworks.jpeg unchanged).As % of libzstd, A → B:
-3-19Validation
go test ./zstd ./huff0 ./fsepass on amd64 (Zen 3) and arm64 (Neoverse-N1). With-tags noasm, zstd passes on both and huff0 on amd64.TestBuildDtableARM64MatchesReferencechecks the arm64 asm against the Go build..sfiles regenerate byte-identically fromgen_fse.go.🤖 Generated with Claude Code
Summary by CodeRabbit