Skip to content

Faster inflate_fast(): 64-bit bit buffer and wide match copies - #793

Open
icodywu wants to merge 1 commit into
madler:developfrom
icodywu:Faster_Decompression
Open

icodywu wants to merge 1 commit into
madler:developfrom
icodywu:Faster_Decompression

Conversation

@icodywu

@icodywu icodywu commented Mar 28, 2023 •

Copy link
Copy Markdown

This branch now holds a new and much smaller version of this change, rebased on develop (1.3.2.1). (Updated on 2026-10-12 with sixteen-byte match copies and eight-byte steps for short distances; the numbers below are for this version.)

Why it changed. The first version rewrote the decoding tables and parts of inflate(). Testing it under ASAN/UBSAN and with damaged streams found real problems in it: distances before the start of the output were not checked until a window existed, length symbols 286/287 were not rejected, the code-length code's table was a local of inflate() and did not survive a return in the middle of a dynamic header, and some copies read before the output buffer. It also did not build with GCC or clang, and it changed inflateCodesUsed(). Please disregard it. This version leaves the table format, inflate() and inflateBack() alone.

What it does.

  • inffast.c: where a 64-bit type is available (Z_U8), inflate_fast() first runs a loop that refills a 64-bit bit buffer eight bytes at a time (one load on little-endian machines, the bytes one by one otherwise), once per length/distance pair or per up to three literals. It copies matches sixteen bytes at a time for distances of 16 or more and eight at a time for 8 to 15; for distances 1 to 7 each step writes eight bytes and advances by the distance; window parts go with zmemcpy(). The loop runs while sixteen bytes of input remain and a match plus fifteen bytes fits in the output; the existing loop then continues unchanged, so the entry conditions of inflate_fast() are the same.
  • inflate.c, infback.c, inftrees.h: root tables of 10 bits for literal/length codes and 8 for distance codes (were 9 and 6), so fewer codes need a second lookup. ENOUGH is from examples/enough.c: 1332 and 400 (1334 and 402 with PKZIP_BUG_WORKAROUND). The inflate state grows by 1152 bytes.
  • test/infcover.c: cover_fast64() covers the new loop and second-level codes with the larger roots. On x86, every line of inffast.c runs except the byte-wise load for big-endian machines (which runs in the s390x test below); infback.c and inftrees.c stay at 100% line coverage (inflate.c at 96%, as before).

Only C89, no new API, no change in behavior: for every input, valid or not, the output and return codes are those of develop.

Speed. inflate() throughput, stock develop -> this branch, AMD EPYC 9334 (Zen 4, 2.7 GHz), one core, GCC 14.2 -O2, streams from zlib's own deflate:

corpus level one call (MB/s) 16K in / 64K out (MB/s)
Silesia 1 411 -> 646 (+57%) 384 -> 566 (+47%)
Silesia 6 438 -> 697 (+59%) 409 -> 617 (+51%)
Silesia 9 442 -> 703 (+59%) 412 -> 620 (+50%)
Canterbury + Calgary 1 447 -> 724 (+62%) 419 -> 629 (+50%)
Canterbury + Calgary 6 468 -> 817 (+74%) 436 -> 709 (+63%)
Canterbury + Calgary 9 474 -> 825 (+74%) 441 -> 718 (+63%)
enwik8 1 343 -> 576 (+68%) 317 -> 478 (+51%)
enwik8 6 349 -> 591 (+69%) 325 -> 507 (+56%)
enwik8 9 350 -> 592 (+69%) 325 -> 506 (+56%)

-O3 gives the same gains (+58% to +76% in one call). On Silesia level 6, GCC 11 gives +61% / +54% and clang 18 +55% / +47%. 32-bit x86 (-m32), where the 64-bit buffer is emulated: +9% in one call, +4% in pieces. Windows, Visual Studio 2022 (cl /O2, x64) on an Intel Core i7-8850H, level 6: +55% to +64% in one call, +46% to +48% in pieces.

Testing.

  • make test; infcover as above; -std=c89 -pedantic without new warnings; ZLIB_DEBUG builds.
  • A stress test on Canterbury, Calgary, four Silesia files and synthetic inputs (runs, short periods, random, tiny), with 11 deflate settings (levels 1/6/9, windows 9/12/15, raw, gzip, Huffman only, RLE, fixed, stored): one call, random input and output pieces each in a buffer of its own size, and inflateBack(); plus 60 damaged copies of each stream, whose return codes, output lengths and CRCs must equal those of unpatched develop (34,860 streams). Clean under ASAN and UBSAN, also with INFLATE_STRICT, INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR and PKZIP_BUG_WORKAROUND. Identical results on x86-64, i386, arm64, ARMv5, ppc64le and s390x (big-endian; QEMU), and with MSVC 2022 (x64).
  • Z_SYNC_FLUSH: every byte before a flush point is delivered once its compressed bytes are in (43,815 messages).
  • Differential fuzzing with libFuzzer against unpatched develop (valid and damaged streams, random input and output pieces): 65 million runs under ASAN and 47 million under UBSAN, no difference and no report.

🤖 Generated with Claude Code

@icodywu

icodywu commented Apr 11, 2023

Copy link
Copy Markdown
Author

Dear Maxpaj,

Would you mind explaining what in your view is bogus? Did you test the code performance?

@nmoinvaz

nmoinvaz commented Apr 11, 2023 •

Copy link
Copy Markdown
Contributor

It might be a good idea to split your PR into multiple functional commits explaining each change in detail. This allows each change to be performance tested individually.

@icodywu

icodywu commented Apr 12, 2023

Copy link
Copy Markdown
Author

I appreciate your constructive comments. The changes are intermingled, so it is difficult to split them into multiple commits in a clear-cut way. How about I amend a document to contrast each of the critical changes and explain the underlying reasons?

@madler

madler commented Apr 12, 2023

Copy link
Copy Markdown
Owner

I am waiting for the proposer's to-do list to be completed, and I will then test the commit for speed and inspect for correctness and portability. I will then consider whether I should merge it or some rewrite of it.I do not require that this be split into multiple commits.

@icodywu

icodywu commented May 3, 2023

Copy link
Copy Markdown
Author

I have created inflateBackWrap() to validate inflateBack(). Moreover, inflateBackWrap() exhibits identical functionality as inflate(). This allows for an apple-to-apple comparison of performance between inflate() and inflateBack() under different setups.
I have also added detailed comments inside the code to explain the underlying rationale.

@madler, could you take over and review the code? Feel free to make changes in your manner.

@hsiangkao

hsiangkao commented Sep 4, 2023 •

Copy link
Copy Markdown

Hi @madler, may I ask what the current status of this work.

we'd like to enable Intel DEFLATE IAA/QAT in-kernel support for EROFS, but we'd also like to have a faster DEFLATE software decompression fallback support anyway.

I'm not sure if zlib official codebase could address that (otherwise zlib-ng codebase has to be considered instead) since DEFLATE-family is currently a de-facto standard for various common use cases/formats, and better optimized (de)compression for those modern processors could benefit all of us.

And I guess old 16-bit platforms are unimportant now since they could still be supported by old stable zlib versions.

@madler

madler commented Sep 4, 2023

Copy link
Copy Markdown
Owner

This is on my to-do list.

@madler
madler changed the base branch from master to develop January 17, 2024 23:36
@icodywu

icodywu commented Oct 25, 2024

Copy link
Copy Markdown
Author

It has been 18 months. Is there anything I can do to expedite the process?

@madler

madler commented Oct 25, 2024

Copy link
Copy Markdown
Owner

It has been 18 months. Is there anything I can do to expedite the process?

No, you've done all the good things. It's on me now.

nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request Apr 8, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request May 5, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request Jul 28, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request Sep 9, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request Sep 9, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request Sep 9, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request Sep 10, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request Oct 4, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
nmoinvaz added a commit to nmoinvaz/zlib-ng that referenced this pull request Oct 8, 2026
Increases the literal/length root table size from 2^10 (1024) to 2^11
(2048) entries. This eliminates secondary table lookups for 11-bit
Huffman codes, which are common in data with flat byte distributions.

Benchmarked on the Silesia corpus, this shows ~10% improvement on osdb
and ~5% on mr, with no regressions on other files. The tradeoff is a
larger ENOUGH_LENS (1332 to 2340), increasing the inflate_state codes
array by 4032 bytes.

inflate_table() already adapts root downward via root = MIN(root, max)
when codes are shorter, so the larger initial value only takes effect
for blocks that actually have codes exceeding 10 bits.

Based on madler/zlib#793 and zlib-ng#1332.
@icodywu
icodywu force-pushed the Faster_Decompression branch from cfa12c8 to ec19b74 Compare October 11, 2026 21:22
@icodywu icodywu changed the title Faster decompression. Faster inflate_fast(): 64-bit bit buffer and eight-byte copies Oct 11, 2026
@icodywu

icodywu commented Oct 11, 2026

Copy link
Copy Markdown
Author

@madler I have replaced this PR's branch with a new version, rebased on develop. Testing the first version under sanitizers found memory-safety bugs in it (details in the description), so please disregard that one. The new change is confined to inffast.c, plus the root table sizes in inflate.c, infback.c and inftrees.h, and tests in infcover.c. It is 43% to 62% faster on Silesia, Canterbury/Calgary and enwik8, gives the same results as develop on every input I could produce (including 85 million differential fuzz runs), and keeps inffast.c at 100% coverage in infcover. Thank you for keeping it on your list.

Where a 64-bit type is available (Z_U8), inflate_fast() first runs a loop that
refills a 64-bit bit buffer eight bytes at a time, once per length/distance
pair or per up to three literals. It copies matches sixteen bytes at a time
for distances of 16 or more and eight at a time for 8 to 15; for distances 1
to 7 it writes eight bytes per step and advances by the distance. It runs while
sixteen bytes of input remain and a match plus fifteen bytes fits in the
output; the existing loop then continues as before, so the entry conditions of
inflate_fast() and everything outside inffast.c stay the same, except the root
table sizes: 10 bits for literal/length codes and 8 for distance codes (were 9
and 6), so that fewer codes need a second lookup. ENOUGH is from
examples/enough.c (1332 and 400), and the inflate state grows by 1152 bytes.

inflate() is 57% to 74% faster with the whole stream in one call and 47% to
63% faster with 16K input and 64K output buffers, on Silesia, Canterbury plus
Calgary, and enwik8 at levels 1, 6 and 9 (AMD EPYC 9334, one core, GCC 14.2
-O2 and -O3). On Silesia level 6 in one call, GCC 11 gives +61% and clang 18
+55%. 32-bit x86 is 9% faster in one call and 4% faster in pieces.

test/infcover.c gains cases for the new loop and for second-level codes with
the larger roots: every line of inffast.c runs except the byte-wise load used
on big-endian machines, and infback.c and inftrees.c stay at 100% line
coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@icodywu
icodywu force-pushed the Faster_Decompression branch from ec19b74 to 217f080 Compare October 13, 2026 00:39
@icodywu icodywu changed the title Faster inflate_fast(): 64-bit bit buffer and eight-byte copies Faster inflate_fast(): 64-bit bit buffer and wide match copies Oct 13, 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.

5 participants