Skip to content

perf(encode): flush the sequence bitstream where upstream flushes it - #497

Merged
polaz merged 2 commits into
mainfrom
perf/#493-entropy-stage
Sep 8, 2026
Merged

perf(encode): flush the sequence bitstream where upstream flushes it#497
polaz merged 2 commits into
mainfrom
perf/#493-entropy-stage

Conversation

@polaz

@polaz polaz commented Sep 8, 2026

Copy link
Copy Markdown
Member

Summary

The sequence loop drained the bit accumulator twice for every sequence, once after the three FSE state diffs and once after the extra-bit fields. Upstream drains once, and asks before the first one whether the extras that follow would actually fail to fit beside the diffs.

Its two conditionals are adopted verbatim (zstd_compress_sequences.c:350 and :355): flush after the diffs only when ofBits + mlBits + llBits >= 64 - 7 - (LLFSELog + MLFSELog + OffFSELog), which with our accumulator logs is 31, and flush before the offset field only past 56 bits of extras. Below the first threshold nothing has to leave, because 7 leftover bits plus 26 of state diffs plus 30 of extras is 63.

Only the flush timing changes. No written bit moves, which is what the byte-identity check below is for.

The three widths those thresholds are derived from are now asserted where the arithmetic happens, together with the aggregate bound the no-flush branch rests on. The diff widths are tallied per sequence for it under cfg(debug_assertions), so release builds carry none of it, and the whole suite exercises it on every fixture and level.

Measurement

i9, three arms alternated in one ssh session, perf stat -r 3, three rounds, per frame of an 8 MiB access log:

level arm cycles instructions bytes
1 ours before 123.99 M 260.90 M 1,467,854
1 ours after 118.69 M 262.57 M 1,467,854
1 libzstd 75.22 M 149.85 M 1,469,809
9 ours before 663.00 M 1,603.91 M 1,161,999
9 ours after 662.97 M 1,604.81 M 1,161,999
9 libzstd 501.13 M 1,038.51 M 1,158,646

Against upstream on cycles that is 1.648x to 1.578x at level 1, and 1.323x either way at level 9. The three readings per arm do not overlap at level 1.

Instructions go up while cycles go down: a compare and a sum replaced an unconditional store plus length commit plus shift, and the clock prefers that trade.

Control arm: incompressible input, whose blocks are written raw so this loop never executes. Its instruction count is identical between the two binaries and its cycles moved 0.6%, which is what the level-9 figure sits inside.

Output is byte-identical to before over 30 fixture-and-level rows: three input shapes (decodecorpus, random, access log) against ten levels from --fast=5 to 22, comparing frame digests rather than lengths.

Also in here

The issue's first lead turned out not to hold for the level it is about, and the issue body now says so: the Fast backend (levels -5..2) never calls encode_offset_with_history, so nothing is computed twice there. The body also records the trap in doing that lead properly, that an offBase precomputed during matching is wrong for any partition later written raw, and a negative result measured on the way here (carrying FSE states in upstream's value form removes 5.07 M instructions a frame and is 5.2% slower at level 9).

Testing

  • cargo nextest run -p structured-zstd -F hash,std,dict-builder: 1053 passed
  • cargo nextest run -p ffi-bench -F bench-internals,dict-builder: 64 passed
  • cargo test --doc: 23 passed
  • cargo clippy --all-targets on both CI feature sets in debug and release, cargo fmt --check: clean

Part of #493.

The sequence loop wrote the three FSE state diffs, drained the accumulator,
then wrote the three extra-bit fields and drained it again. Upstream drains
once, and asks before the first one whether the extras that follow would
actually fail to fit beside the diffs: ofBits + mlBits + llBits >= 64 - 7 -
(LLFSELog + MLFSELog + OffFSELog), which with our accumulator logs is 31
(zstd_compress_sequences.c:350). Under it nothing has to leave, because 7
leftover bits plus 26 of state diffs plus 30 of extras is 63. The second
conditional is upstream's too: past 56 bits of extras the offset field needs
its own container (:355).

So the common sequence now costs one drain instead of two, and the drain is a
store, a length commit and a shift.

Per frame on an 8 MiB access log at level 1 (i9, two prebuilt binaries
alternated in one session, perf stat -r 3, three rounds, ranges not
overlapping): 124.26 M cycles becomes 118.50 M, -4.6%. Retired instructions
went the other way, 260.90 -> 262.57 M (+0.6%), which is the compare and the
sum that replaced the unconditional drain: fewer stores, slightly more
arithmetic, and the clock prefers it. Level 9 on the same fixture moved
+0.36%, inside what the control arm drifted by. The control is incompressible
input, whose blocks are written raw so this loop never runs; its instruction
count is identical between the two binaries and its cycles moved 0.6%.

Output is byte-identical over 30 fixture-and-level rows (three shapes, ten
levels from --fast=5 to 22), which is the whole claim: only the flush timing
changed, never a written bit.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 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-09-08T10:55:34.427202Z 1659343 New commits
ℹ️ 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 Sep 8, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 42 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 5642b601-6637-49f9-8743-149c80c19bf7

📥 Commits

Reviewing files that changed from the base of the PR and between c2d7676 and 1659343.

📒 Files selected for processing (1)
  • zstd/src/encoding/blocks/compressed.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 7e9ca26a-3331-4251-9d35-a56e19a375fd

📥 Commits

Reviewing files that changed from the base of the PR and between e06ac22 and c2d7676.

📒 Files selected for processing (1)
  • zstd/src/encoding/blocks/compressed.rs

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


📝 Walkthrough

Walkthrough

The sequence encoder now uses conditional bit-accumulator flushes based on total extra-bit usage. The thresholds match upstream zstd logic. The emitted bit stream remains unchanged, while typical sequences use fewer flushes.

Changes

Sequence encoding

Layer / File(s) Summary
Conditional sequence flushes
zstd/src/encoding/blocks/compressed.rs
The encoder calculates extra_bits_total, flushes when it reaches 31 bits, and flushes before the offset field when it exceeds 56 bits. Comments document the updated bit budget and bounds.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to c2d76

Sequence encoding now flushes at upstream-compatible bit thresholds while preserving the emitted stream. Current evidence indicates no unresolved merge-blocking risk.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: updating sequence bitstream flushing to match upstream behavior. It is concise and specific.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/#493-entropy-stage

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 Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2d767618f

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zstd/src/encoding/blocks/compressed.rs
Comment thread zstd/src/encoding/blocks/compressed.rs
The 31 and 56 thresholds are arithmetic on three widths: the FSE state
diffs at most 26 bits together, the literal-length and match-length extras
at most 16 each, the offset extras at most 31. Those bounds lived only in
the comment, so an encoder that widened its output would first show up as
an accumulator assertion further down, or in a release build as a corrupted
stream with nothing pointing at the cause.

They are now asserted where the arithmetic happens, including the aggregate
the no-flush branch actually rests on: 7 leftover bits plus the diffs
actually written plus all three extra fields must fit in 64. The diff widths
are tallied per sequence for it, under cfg(debug_assertions), so release
builds carry none of this. The whole suite exercises it on every fixture and
level.

Also records the upstream arm the flush-schedule commit was missing. Same
fixture, same session, same three rounds, per frame of the 8 MiB access log:

  level 1   ours before 123.99 M cycles / 260.90 M insn
            ours after  118.69 M cycles / 262.57 M insn
            libzstd      75.22 M cycles / 149.85 M insn
            so 1.648x -> 1.578x of upstream on cycles

  level 9   ours before 663.00 M / 1,603.91 M
            ours after  662.97 M / 1,604.81 M
            libzstd     501.13 M / 1,038.51 M
            1.323x either way

Bytes on the same runs: 1,467,854 against upstream's 1,469,809 at level 1,
1,161,999 against 1,158,646 at level 9.
@polaz
polaz merged commit a3d112b into main Sep 8, 2026
27 checks passed
@polaz
polaz deleted the perf/#493-entropy-stage branch September 8, 2026 11:25
@sw-release-bot sw-release-bot Bot mentioned this pull request Sep 8, 2026
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.

1 participant