Skip to content

perf(decode): adopt libdeflate's decoder architecture — tracking issue #2668

Description

@kim-em

Umbrella / tracking issue: re-architect the native DEFLATE decoder along libdeflate's lines. Records what libdeflate actually does (correcting #2653's premise) and links the per-phase work, split by the lean4#14053 dependency.

Background: why #2653 didn't pan out, and what libdeflate actually does

#2653 assumed libdeflate decodes "1-2 symbols per table lookup". Both prototypes built on that premise were net-negative (closed on #2653): a lazy runtime second-lookup was −1.7%/−2.9% (canterbury/dickens), and an eager precomputed wide 2-symbol table was −31% to −53%, dominated by per-block table-build cost.

Reading libdeflate's source (lib/deflate_decompress.c, lib/decompress_template.h) shows the premise was wrong. libdeflate decodes one symbol per table lookup; its speed (multiple GB/s vs our ~143 MB/s) comes from a different, composable set of techniques:

  1. Bulk word refill — a single unaligned 8-byte load OR'd into the bit buffer (REFILL_BITS_BRANCHLESS). We load one byte at a time in a ~7-iteration loop.
  2. Multiple symbols per refill — the fastloop refills once (~56 bits) then decodes up to 3 literals back-to-back, no refill between them. We refill after every symbol.
  3. Packed single-word entry — one u32 per entry (literal/length-base + codeword length + extra-bit count + type flags); literal fast path is a single high-bit test. We use two arrays plus separate length-base/extra lookups.
  4. Canonical O(num_syms + 2^bits) build, no Huffman tree — counting sort + table-doubling fill, with subtables (array lookups) for long codes. We build a HuffTree per block, walk it per slot to build the table, and tree-walk long codes.
  5. Wider root tableLITLEN_TABLEBITS = 11 vs our fastBits = 9.

What our bottleneck actually is (corrected after the #2669 attempt)

libdeflate's per-symbol cost is a few register ops, so amortizing the refill frequency (technique 2) is a big deal for them. Ours is not: an implementation attempt at technique 2 (#2669) regressed on every variant, because our per-symbol cost is dominated by table reads + ByteArray.push/RC + well-founded-recursion argument threading, which batching doesn't reduce — and without packed entries the per-literal "continue the batch?" test costs the same multi-read+compare that sank #2653's lazy prototype. So technique 2 is not the safe first win; it depends on technique 3 and is marginal even then.

The levers that attack our cost structure are the ones that cut per-symbol reads, per-refill cost, and per-block build: techniques 1, 3, 4.

Phases — split by the lean4#14053 dependency, measure-first at each gate

Priority, available now (no lean4#14053):

Deferred / conditional:

Blocked on lean4#14053 (wide ByteArray load accessors; open):

Recommended order: #2670 (packed entry) first — it's a standalone per-symbol read reduction and unblocks everything else — then measure; #2630 (bulk refill) whenever lean4#14053 is available; then decide whether the remaining gap justifies #2671's heavy build/proof. #2669 is conditional on #2670 and low priority.

References: libdeflate lib/deflate_decompress.c (build_decode_table; entry format ~L425-500) and lib/decompress_template.h (fastloop ~L337-435). Foundations landed: #2666.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions