Repository navigation
fix(decoding): keep the scalar sequence decoder's debug frame off a kernel stack - #551
Conversation
At -O0 LLVM gives every inlined temporary its own stack slot, so the
scalar monolith `decode_and_execute_sequences_scalar` grows with each
force-inlined copy. Own frame of each monomorph, measured from the
object file of a debug build for x86_64-unknown-none with
`panic = "abort"` and `default-features = false`:
main BIT_MASK static + executor not forced
UserSliceBackend 147,912 104,648 25,736
FlatBuf 88,648 45,384 23,784
RingBuffer 83,912 40,648 23,784
Two causes, both translated from upstream zstd's own shape:
- `cpu_kernel::BIT_MASK` was a `const [u64; 256]`. A `const` is a value
copied at each use; at -O0 each inlined `mask_lower_bits` load
memcpy'd the 2 KiB table into its own slot (43 KiB of the frame).
Upstream's `BIT_mask` is `static const unsigned BIT_mask[]`
(lib/common/bitstream.h), one object; it is now a `static`.
- `execute_one_sequence_pipelined` was `#[inline(always)]`, and the
monolith expands it at the three `execute_one_body!` sites, each
pulling in the backend's whole inline exec tree. Upstream marks
`ZSTD_execSequence` `HINT_INLINE` and lets the build drop the forced
inline (`ZSTD_NO_INLINE`, lib/common/compiler.h). The forced inline now
applies only without `debug_assertions`; a debug build calls it
(26,376 B for UserSliceBackend). Optimized builds keep the forced
inline.
No storage moved: the frame was many small slots, not one scratch
array. Decoded bytes and error behaviour are unchanged.
The `static` does reach release code. Timing with `profile_decode`
against main, separately built, min of seven interleaved runs on an
aarch64 host: z000033 0.700 s vs 0.700 s, z000091 0.229 s vs 0.229 s,
z000006 (small frame) 3.713 s vs 3.704 s, all inside the build-to-build
band. The x86_64 scalar tier was not timed.
Regression test `scalar_decode_fits_a_bounded_debug_stack` enters the
scalar monolith for all three backends on a 96 KiB thread; a frame that
does not fit hits the guard page and aborts the test binary. Mutation
check on main: restoring the `const` alone, the forced inline alone, or
both each aborts it with "has overflowed its stack"; with the fix it
passes.
|
Important Review skippedReview was skipped as selected files did not have any reviewable changes. ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to The debug-stack regression test runs in the documented test configuration; no actionable issue remains before merge. Pre-merge checks |
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Thank you for the pull request. Before it can be merged, every author of its commits signs the Contributor License Agreement once. You keep the copyright in your contribution; the agreement is a license to the maintainer. Not signed yet: @myers The status updates by itself once everyone has signed. |
There was a problem hiding this comment.
🔇 Additional comments (3)
zstd/src/cpu_kernel.rs (1)
505-518: LGTM!zstd/src/decoding/sequence_section_decoder.rs (1)
518-519: LGTM!zstd/src/decoding/sequence_section_decoder/tests.rs-399-474 (1)
399-474: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.Stack-overflow regression test depends on the exact
-O0profile.The test aborts the whole test process, not only itself, if the stack overflows. This is intended. The test also stays valid only when the test profile keeps
opt-level = 0. A profile override withopt-levelabove 0 would pass the test and hide a regression. Consider acfg(debug_assertions)gate or a comment that names this dependency. The current doc comment already states thatcargo testbuilds at-O0, so this is optional.The test code is otherwise correct.
stduse is allowed in test code by the coding guidelines. The leaked 4 KiB slice is acceptable in a test.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: structured-world/structured-zstd/.coderabbit.yaml
- Review profile: ASSERTIVE
- Plan: Advanced
- Run ID:
70be2393-86f2-4429-8697-f72d5684cf31
📒 Files selected for processing (3)
zstd/src/cpu_kernel.rszstd/src/decoding/sequence_section_decoder.rszstd/src/decoding/sequence_section_decoder/tests.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.
## 🤖 New release * `structured-zstd`: 0.1.1 -> 0.2.0 <details><summary><i><b>Changelog</b></i></summary><p> <blockquote> ## [0.2.0](v0.1.1...v0.2.0) - 2026-10-12 ### Added - *(cli)* --trace writes one CSV line per frame ([#579](#579)) - *(decoding)* [**breaking**] hand the source back from a refused dictionary ([#577](#577)) - *(decoding)* [**breaking**] configurable window ceiling up to 2 GiB ([#568](#568)) ### Fixed - *(decoding)* box the FSE sequence tables so decode fits a 32 KiB stack ([#550](#550)) - *(decoding)* keep the scalar sequence decoder's debug frame off a kernel stack ([#551](#551)) ### Performance - *(cli)* move data in 128 KiB pieces, not 8 KiB ([#573](#573)) </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
-O0) build of the scalar sequence decoder no longer needs ~145 KiB of stack for one frame. Debug kernels run with small fixed task stacks too, and on main the scalar monolith's own frame is larger than a 96 KiB thread.constlookup table copied at every use, and an executor that is forced inline even in a debug build.BIT_MASKbecoming astatic.Changes
cpu_kernel::BIT_MASKis astaticinstead of aconst [u64; 256]. Aconstis a value, so at-O0each inlinedmask_lower_bitsload copied all 2 KiB of it into its own stack slot (43 KiB of the frame). Upstream'sBIT_maskisstatic const unsigned BIT_mask[](lib/common/bitstream.h).execute_one_sequence_pipelinedis#[inline(always)]only withoutdebug_assertions, and plain#[inline]with them. The monolith expands it at the threeexecute_one_body!sites. Upstream marksZSTD_execSequenceHINT_INLINEand drops the forced inline underZSTD_NO_INLINE(lib/common/compiler.h).scalar_decode_fits_a_bounded_debug_stackenters the scalar monolith forRingBuffer,FlatBufandUserSliceBackendon a 96 KiB thread.cargo testbuilds at-O0, so it runs the configuration a debug kernel links. Mutation check: restoring theconstalone, the forced inline alone, or both each aborts it with "has overflowed its stack".Stack
Own frame of each monomorph of
decode_and_execute_sequences_scalar, read from the object file of a debug build forx86_64-unknown-none,panic = "abort", no default features:BIT_MASKstaticThe out-of-line executor adds 26,376 B (
UserSliceBackend) when called.Performance
profile_decodeagainst main, separately built binaries, min of seven interleaved runs, aarch64 (Apple silicon):All inside the build-to-build band; no change established in either direction. The x86_64 scalar tier was not timed, and not measured against libzstd.
Testing
fmt; clippy with
-D warnings(all targets; library without default features); the crate tests on macOS aarch64.Summary by CodeRabbit