Decompress: optimize sequence decompression (fast-path inlining, ABI scalarization, branch elision) - #4751
Decompress: optimize sequence decompression (fast-path inlining, ABI scalarization, branch elision)#4751JoeAzar wants to merge 1 commit into
Conversation
…scalarization, branch elision)
|
Hi @JoeAzar! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
cc @danlark1 |
Verified Google Fleet Production Impact (CPU)
Summary & Motivation
Sequence decoding and execution form the primary CPU hotspot during Zstandard block decompression. In high-throughput workloads, modern out-of-order execution pipelines frequently stall on avoidable microarchitectural bottlenecks within
lib/decompress/zstd_decompress_block.c:seq_tstruct by value forces compilers to classify it asMEMORYclass under the x86-64 System V ABI (and exceeds register budgets on other ABIs), resulting in defensive stack spills and store-to-load forwarding (STLF) stalls.const BYTE** litPtrby reference breaks compiler escape analysis. BecauselitPtr's address is exposed across function boundaries, compilers defensively spill and reload the pointer to/from memory on every loop iteration.hasExtDictis known at block start.seqState) to be spilled.This PR introduces an orchestrated set of microarchitectural optimizations addressing all five bottlenecks. Across microbenchmarks and multi-corpus decompression suites, these changes deliver a 1.19x – 1.23x speedup (+23% decompression throughput) with zero changes to the on-disk format or external APIs.
Detailed Architectural Changes
1. Scalar Parameter Flattening & Calling Convention Register Compliance
seq_tis a 24-byte structure (litLength,matchLength,offset). Under x86-64 System V ABI, structures larger than 16 bytes cannot be passed in registers and must be pushed onto the stack. Combined with 7 other parameters passed toZSTD_execSequence*, callers exceeded the 6-register passing limit.seq_tat function boundaries into three scalarsize_targuments:litLength,matchLength, andoffset.prefixStart,virtualStart,dictEnd) into a singleconst ZSTD_DCtx* dctxpointer.2. Escapeless Literal Pointer Promotion (Value Semantics for
litPtr)ZSTD_execSequence*acceptedconst BYTE** litPtrso that the callee could advance the pointer. Taking&litPtrin the caller loop causedlitPtrto escape, preventing the compiler from keeping it pinned to a CPU register across iterations.ZSTD_execSequence*to takeconst BYTE* litPtrstrictly by value.litPtr += sequence.litLength) directly into the caller loops.litPtrachieves permanent register residency throughout the entire decompression loop.3. External Dictionary Elision via Template Specialization (
hasExtDict)offset > oLitEnd - prefixStart) on every sequence, even when no external dictionary was loaded.const int hasExtDictspecialization parameter toZSTD_decompressSequences_body,ZSTD_decompressSequences_bodySplitLitBuffer, andZSTD_decompressSequencesLong_body.hasExtDict = (dctx->prefixStart != dctx->virtualStart)once per block and dispatch the constant (0 or 1) to the body functions.hasExtDict == 0), the compiler dead-code-eliminates the external dictionary branch and its pointer arithmetic out of the inner loop, while retaining the essential corruption check (offset > oLitEnd - virtualStart).4. Branch-Separated Small Offset & RepCode Decoding (
ZSTD_decodeSmallOffset)ZSTD_decodeSequenceandZSTD_decodeSequenceLongdecoded small offsets and RepCodes via nested ternary operators handlingofBits == 0andofBits == 1, serializing dependency chains.ZSTD_decodeSmallOffset.ofBits == 0case with an early return branch, completely bypassing deeper logic.ofBits == 1, replaced nested ternary expressions with sequential conditional assignments (if (offset_eq1 >= 2) ...), allowing compilers to emit optimal, zero-spill registercmovinstructions.5. Inlined Sequence Execution Fast-Path in
ZSTD_decompressSequences_bodyZSTD_execSequenceon every iteration causes heavily mutated bitstream decoder states (such asseqState.DStreamand FSE states) to be preserved across call boundaries.ZSTD_decompressSequences_body.oend - WILDCOPY_OVERLENGTH,offset >= WILDCOPY_VECLEN, match remains within prefix) execute direct 16-byte copy + wildcopy for literals and wildcopy for match directly in the loop.< WILDCOPY_VECLEN, external dictionaries, tail sequences nearoend_w) cleanly fall back toZSTD_execSequence.Safety & Invariants Preserved
RETURN_ERROR_IF(UNLIKELY(offset > (size_t)(oLitEnd - virtualStart)), corruption_detected, "")) remain strictly enforced on all paths.!MEM_32bits() || (size_t)(oend - op) >= sequence.litLength + sequence.matchLength + WILDCOPY_OVERLENGTH).ZSTD_splitmodes inZSTD_decompressSequences_bodySplitLitBufferandZSTD_decompressSequencesLong_bodyare fully updated and tested with identical scalar register semantics.FUZZING_ASSERT_VALID_SEQUENCEandFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTIONtest guards remain intact.Benchmark Results
Benchmark 1: Core Decompression (
BM_COMPRESSION_ZSTD_DECOMPRESS) (x86-64,--runs=15)Benchmark 2: Google Fleetbench (
BM_DecompressSink) (x86-64,--runs=10)