Skip to content

Decompression: match C's codegen in a few hot spots - #501

Open
jesup wants to merge 14 commits into
trifectatechfoundation:mainfrom
jesup:decompress-codegen-parity
Open

jesup wants to merge 14 commits into
trifectatechfoundation:mainfrom
jesup:decompress-codegen-parity

Conversation

@jesup

@jesup jesup commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Builds on PR #500 (decompress-dictionaries), which builds on #499; only the last 3 commits are new.

  • Enable lzcnt and bmi1 alongside bmi2 in the dynamically dispatched functions, as C's BMI2_TARGET_ATTRIBUTE does; without lzcnt, ilog2 in the table builders compiles to bsr plus a fixup. The runtime check now also requires lzcnt.
  • FSE state lookup: index the table instead of masking the state. The mask sits on the state dependency chain; the bounds check does not, and it never fails because every table is built by this crate.
  • HUF 4X2 fast loop: load each table entry with one u32 load instead of three field loads (one instruction less per symbol on aarch64; x86 on unix uses the asm loop).

These only pay off once #PR1 is in; on main alone the effect is lost in the cold-code penalty.

Measured on Zen 3 with a harness that decodes the way Firefox does (fresh DStream per response, 16 KB input chunks, 128 KB output buffer), cycles per decompressed byte. "Cold PGO" is instrumented with a profile that never decodes zstd, which is what Firefox ships, since its PGO training has no zstd content; "trained" includes zstd decoding in the training.

#PR3 this PR
web resources, cold PGO 3.07 2.99 (-2.5%)
small responses, cold PGO 6.28 6.06 (-3.5%)
silesia level 19, cold PGO 3.43 3.35 (-2.4%)
dictionary, cold PGO 7.92 7.68 (-3%)
web resources, trained PGO 2.96 2.90 (-2%)
small responses, trained PGO 5.98 5.78 (-3%)
dictionary, trained PGO 7.95 7.63 (-4%)

Upstream benchmark (non-PGO, rs/C): silesia-small 0.995 -> 0.992, re2-exhaustive 1.022 -> 1.012, zip64support 0.991 -> 0.983.

On an Apple M-series Mac (wall-clock, same harness), this PR and the PRs below it decode about 9% faster than main without PGO, 14% faster with cold PGO and 8% faster with trained PGO (about 10%, 16% and 9% for streaming non-dictionary inputs).

Each commit builds, is rustfmt-clean and passes cargo test.

@codecov

codecov Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.87755% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/decompress/zstd_decompress_block.rs 85.48% 9 Missing ⚠️
Flag Coverage Δ
test-aarch64-apple-darwin 51.28% <93.75%> (+0.54%) ⬆️
test-aarch64-unknown-linux-gnu 49.34% <93.75%> (+0.02%) ⬆️
test-i686-unknown-linux-gnu 49.16% <93.19%> (+0.03%) ⬆️
test-x86_64-unknown-linux-gnu 51.02% <93.19%> (+0.52%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
lib/common/bitstream.rs 98.76% <ø> (ø)
lib/common/entropy_common.rs 94.23% <ø> (ø)
lib/common/fse_decompress.rs 94.73% <100.00%> (+0.09%) ⬆️
lib/common/zstd_internal.rs 87.01% <100.00%> (+0.34%) ⬆️
lib/compress/huf_compress.rs 90.94% <ø> (ø)
lib/compress/zstd_compress_sequences.rs 91.01% <ø> (ø)
lib/decompress/huf_decompress.rs 84.03% <100.00%> (+0.64%) ⬆️
lib/decompress/zstd_decompress.rs 67.18% <100.00%> (+11.57%) ⬆️
lib/decompress/zstd_decompress_block.rs 76.85% <85.48%> (-0.23%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jesup
jesup force-pushed the decompress-codegen-parity branch from fcfc7fb to 0297d6e Compare November 6, 2026 16:52
jesup added 14 commits November 10, 2026 17:01
LLVM lowers inline thresholds for code that is cold in a PGO profile, which
leaves calls like `Writer::subslice` in the sequence loop. Firefox PGO
training does not exercise zstd, so this is the configuration it ships.
PGO builds turned the element-wise `fill` into two byte stores per entry.
LLVM vectorized the per-byte loop on x86-64 only without PGO, and not at all
on aarch64.
`ZSTD_execSequence` already guarantees the sequence fits before `oend`, so the
per-sequence `Writer` bounds check is redundant. Keep `new_seq_state` out of
line: inlining it makes the x86-64 loop allocate registers noticeably worse.

On targets other than x86_64, force it inline with the new `pgo` feature:
out of line the sequence state stays in memory, which made trained and cold
PGO builds up to 10% slower on Apple silicon. Without PGO forcing it inline
is slower, so the compiler decides. The long sequence decoder always calls
the out of line version.
The 16-byte over-copy relies on the same output slack as wildcopy.
Makes `is_last_sequence` a constant in the loop.
Matches C's BMI2_TARGET_ATTRIBUTE; without lzcnt, `ilog2` in the table
builders compiles to bsr plus a fixup.
The mask sits on the state dependency chain; the bounds check does not, and
never fails because every table is built by this crate.
@jesup
jesup force-pushed the decompress-codegen-parity branch from 0297d6e to 9d73ef9 Compare November 10, 2026 17:13
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