Skip to content

perf(encode): bring the level-1 entropy stage to libzstd's pace - #628

Merged
polaz merged 5 commits into
mainfrom
perf/#493-entropy-loops
Oct 25, 2026
Merged

polaz merged 5 commits into
mainfrom
perf/#493-entropy-loops

Conversation

@polaz

@polaz polaz commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Brings the level-1 entropy stage to within a few percent of libzstd: on an 8 MiB access log at level 1 the whole encode goes from 1.15x libzstd's cycles to about 1.07x, and what remains is the match finder plus the literal copy, not the entropy stage.
  • Every change leaves the compressed output byte-identical.
stage (8 MiB log, level 1) ours libzstd
sequence encoding encode_sequences 291 M ZSTD_encodeSequences 300 M
sequence codes and their histograms fill_and_count 112 M ZSTD_seqToCodes 50 M + code counts inside HIST
literals (Huffman) 211 M 197 M
match finder and record 1,860 M ZSTD_compressBlock_fast 1,880 M (includes the literal copy)
literal copy gather 149 M inside the match finder

Changes

  • FSE encoder: the per-symbol transform is 8 bytes like upstream's symbolTT (i32 find-state delta), and the next-state slot, inside the table by construction, is read without a bounds check (debug-asserted).
  • Huffman literal stream: the packed code table always holds 256 entries and is passed as &[u64; 256], and the main loop walks constant-length chunks, so neither the symbol load nor the code lookup carries a bounds check.
  • Sequence record: the built-in matcher's reports skip the ordering and 32-bit checks that guard a custom matcher (kept as debug assertions); the record now inlines into the Fast kernel. A custom matcher keeps the checked path.
  • Literal gather: a run's first chunk is copied unconditionally and only the rest of a longer run loops, upstream's ZSTD_wildcopy shape.
  • build_table_from_symbol_counts is compiled for tests only; the fuzz-exports build carried it as dead code.

Testing

Frames are byte-identical to main across 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

  • Performance
    • Improved the efficiency of compression sequence handling and Huffman encoding, including for literal data and fixed-size chunks.
    • Streamlined processing of built-in match results while preserving the existing handling of custom match results.
  • Reliability
    • Strengthened debug-time checks for encoding table bounds and sequence ordering.

polaz added 5 commits October 25, 2026 18:15
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
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-25T18:11:09.328835Z 1d33c23 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 971dbc4 and 1d33c23.

Files selected for processing: 6
  • zstd/src/encoding/blocks/compressed.rs
  • zstd/src/encoding/blocks/compressed/literal_runs.rs
  • zstd/src/encoding/blocks/compressed/tests.rs
  • zstd/src/fse/fse_encoder.rs
  • zstd/src/huff0/huf_cstream.rs
  • zstd/src/huff0/huff0_encoder.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The changes update sequence recording and literal copying in compressed blocks, FSE state arithmetic, and Huffman encoding. Huffman packed-code interfaces now use fixed-size arrays, and the unrolled encoder traverses fixed-size chunks.

Changes: Entropy encoding

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: Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Possibly related PRs

Merge Risk: Minimal · up to 1d33c

No actionable issue remains from the reviewed changes; the PR is mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Out of Scope Changes check Warning The change from #[cfg(any(test, feature = "fuzz-exports"))] to #[cfg(test)] removes build_table_from_symbol_counts from the internal fuzz-exports build surface. The linked issue requests level… Restore the feature = "fuzz-exports" configuration for build_table_from_symbol_counts, or provide a directly linked coding requirement that justifies removing this fuzz-target build surface.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed 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 li…
Linked Issues check Passed Issue #493 requires 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,…
Docstring Coverage Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 6 files.
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 #493 requires 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)] removes build_table_from_symbol_counts from the internal fuzz-exports build surface. The linked issue requests level-1 encoding performance and output preservation. It does not require a fuzz-build API change. The manifest identifies fuzz-exports as 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 for build_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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Oct 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@polaz
polaz merged commit 4248d08 into main Oct 25, 2026
35 checks passed
@polaz
polaz deleted the perf/#493-entropy-loops branch October 25, 2026 20:32
@sw-release-bot sw-release-bot Bot mentioned this pull request Oct 25, 2026
polaz pushed a commit that referenced this pull request Oct 26, 2026
## 🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(encode): level-1 entropy stage is 2.8x libzstd on real data; the match finder is not the gap

1 participant