Repository navigation
perf(encode): bring the level-1 entropy stage to libzstd's pace - #628
Conversation
The per-symbol transform packs into 8 bytes as upstream's symbolTT does (i32 deltaFindState beside the u32 deltaNbBits), and the next-state slot, inside the table by construction, is read without a bounds check; a debug assertion holds the invariant in every test build. runner4 (E5-1650 v4, PMU), one-shot loop, 3 interleaved rounds against the previous head, decode control flat: - log8m L1: instructions -3.3%, cycles median -3% - z000033 L1: instructions -2.2%, cycles -2..-4%, no overlap - z000033 L9: instructions -0.6%, cycles ~-2%, no overlap Part of #493
The packed code table always holds 256 entries, as upstream's CTable, so a symbol byte indexes it with no bounds check, and the main loop walks the stream in constant-length chunks so the symbol loads carry no check either. Each symbol paid two compares and two branches; the ten-symbol loop body drops from about 130 instructions to about 80. Frames byte-identical across levels -5..22 and dictionary frames. runner4, one-shot loop, 3 interleaved rounds against the previous head, decode control flat: - z000033 L1: instructions -8.5%, cycles -3.4%, no overlap - log8m L1: instructions -5.5%, cycles within noise - z000033 L9: instructions -1.0%, cycles within noise Part of #493
…ecks The record step checked every reported sequence for literals out of order and for lengths past 32 bits. Both guard a matcher from outside the crate; the built-in driver holds them by construction (trailing literals last, lengths bounded by the block), so its reports are recorded with the checks as debug assertions. A custom matcher keeps the checked path. Frames byte-identical across levels and dictionary frames. runner4, one-shot loop, 3 interleaved rounds against the previous head, decode control flat: - log8m L1: instructions -5.6%, cycles about -3%, no overlap - z000033 L1: instructions -4.4%, cycles about -2.4%, no overlap Part of #493
build_table_from_symbol_counts is crate-private and called only by tests, so the fuzz-exports build carried it as dead code and warned.
The gather rounded every run up to its chunk multiple and entered the chunk loop, about twenty instructions of setup in front of what is a single 32-byte copy for most runs. It now copies one chunk outright and loops only over the rest of a longer run, upstream's ZSTD_wildcopy shape; an empty run takes the same one copy instead of a test. Frames byte-identical across levels and dictionary frames. runner4, one-shot loop: instructions log8m L1 -0.9%, z000033 L1 -0.65%, L3 -0.3%; the host was loaded during the run, so no cycle delta is claimed. Part of #493
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Built-in sequence recording zstd/src/encoding/blocks/compressed.rs |
Built-in matcher reports use a dedicated recorder that accumulates trailing literals and stores sequence lengths and offsets as u32. Custom matcher reports continue to use the checked recorder. |
Chunked literal copying zstd/src/encoding/blocks/compressed/literal_runs.rs |
The chunked path issues one fixed-size wild copy. For runs greater than one chunk and below 2048 bytes, it also copies a rounded-up remainder. Large runs retain the memcpy path. |
FSE state offsets zstd/src/fse/fse_encoder.rs |
FSE state offsets and slot arithmetic use i32. next_state adds a debug bounds assertion before unchecked table access. The table-building helper is now compiled only for tests. |
Fixed-size Huffman packed codes zstd/src/huff0/huff0_encoder.rs, zstd/src/huff0/huf_cstream.rs, zstd/src/encoding/blocks/compressed/tests.rs |
Huffman packed-code storage and encoder parameters use 256-entry arrays. The unrolled encoder processes fixed-size chunks from the end of the input. The table test compares packed-code slices. |
Priority:
Estimated code review effort: 3 (Moderate) | ~25 minutes
Change: Refactor
Possibly related PRs
- structured-world/structured-zstd#626: Both PRs change the entropy-encoding paths in
compressed.rs,literal_runs.rs,huff0_encoder.rs, andhuf_cstream.rs.
Merge Risk: 
Minimal · up to 1d33c
-
No actionable issue remains from the reviewed changes; the PR is mergeable after normal checks.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Title check
-
Explanation
The title clearly and concisely describes the main change: performance improvements to the level-1 encoding entropy stage. The changes target FSE encoding, Huffman encoding, sequence recording, and literal gathering.
Full details: Linked Issues check
-
Explanation
Issue
#493requires level-1 entropy-stage speed work without changing compressed output. The diff optimizes FSE state transforms, Huffman table access and chunk traversal, built-in sequence recording, and literal gathering. It keeps the checked path for custom matchers. The PR reports byte-identical frames and passing automated tests across the required levels and fixtures. The issue's offBase idea is described as a separate potential optimization, so this PR does not need to implement it.
-
Full details: Out of Scope Changes check
-
Explanation
The change from
#[cfg(any(test, feature = "fuzz-exports"))]to#[cfg(test)]removesbuild_table_from_symbol_countsfrom the internalfuzz-exportsbuild surface. The linked issue requests level-1 encoding performance and output preservation. It does not require a fuzz-build API change. The manifest identifiesfuzz-exportsas an internal feature that widens the API for fuzz targets, so this change is not part of the stated encoding objective.Resolution
Restore the
feature = "fuzz-exports"configuration forbuild_table_from_symbol_counts, or provide a directly linked coding requirement that justifies removing this fuzz-target build surface.
-
Finishing Touches
-
📝 Generate docstrings
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
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 @coderabbitai help to get the list of available commands.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
## 🤖 New release * `structured-zstd`: 0.2.5 -> 0.2.6 <details><summary><i><b>Changelog</b></i></summary><p> <blockquote> ## [0.2.6](v0.2.5...v0.2.6) - 2026-10-26 ### Added - *(encode)* faster entropy stage and a lazy match-finder switch ([#626](#626)) ### Performance - *(ldm)* close the level-1 long-distance gap to libzstd ([#632](#632)) - *(encode)* keep the level-1 bit writers' flushes and FSE states off the critical path ([#630](#630)) - *(encode)* bring the level-1 entropy stage to libzstd's pace ([#628](#628)) </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/). Co-authored-by: sw-release-bot[bot] <255865126+sw-release-bot[bot]@users.noreply.github.com>
Summary
encode_sequences291 MZSTD_encodeSequences300 Mfill_and_count112 MZSTD_seqToCodes50 M + code counts insideHISTZSTD_compressBlock_fast1,880 M (includes the literal copy)gather149 MChanges
symbolTT(i32find-state delta), and the next-state slot, inside the table by construction, is read without a bounds check (debug-asserted).&[u64; 256], and the main loop walks constant-length chunks, so neither the symbol load nor the code lookup carries a bounds check.ZSTD_wildcopyshape.build_table_from_symbol_countsis compiled for tests only; thefuzz-exportsbuild carried it as dead code.Testing
Frames are byte-identical to
mainacross levels -5..22 on four fixture shapes and on 2,090 dictionary frames. Formatting, the CI clippy configurations, rustdoc, the full test suite and doctests pass on aarch64 (macOS) and x86_64 (Linux, BMI2/AVX2 tier). Per-change cycle and instruction deltas are in the commit messages.Closes #493
Summary by CodeRabbit