Skip to content

Built-in engines: faster LZMA / LZMA2 decoding (1.2–1.5×) - #1527

Merged
KnifeLemon merged 1 commit into
builtin-enginesfrom
p4/b13-lzma-speed
Nov 14, 2026
Merged

KnifeLemon merged 1 commit into
builtin-enginesfrom
p4/b13-lzma-speed

Conversation

@KnifeLemon

Copy link
Copy Markdown
Owner

Built-in engines: faster LZMA / LZMA2 decoding, used by the 7z, xz / LZMA and NSIS readers. Same output, errors, limits and cancellation granularity.

What changed (Archives/Lzma/LzmaDecoder.cs, RangeDecoder.cs)

  • Symbols decode into the window only; output is copied from the window in blocks (before: every byte went to the window and to the output).
  • Literal runs and match runs are separate methods (LiteralRun, MatchRun) so range, code, input position and state stay in registers for the whole run; with one big method the JIT spilled the range coder to the stack (checked in the disassembly).
  • One probability array accessed through range-checked refs; literal and tree bits decode with masks instead of branches (same probability update); literal decoding reads both child probabilities before the bit is known.
  • Input: with ≥ 64 bytes left (a symbol needs at most 48) bytes are read without checks; only chunk / stream tails take the checked path; streams are refilled with TopUp.
  • Match copies pick 8-byte steps, memmove or fill by distance. BCJ2's DecodeBit is unchanged.

Numbers (CPU time, median of 3, MB/s; the PC was shared, ±15 %; old/new alternated in one process are more reliable than the absolute values)

Input Before After 7-Zip t -mmt=1
Text xz (repo sources, -mx9, 36 MB) 24.6 36.8 77
Mixed xz (Tesseract files + sources, -mx9, 273 MB) 35.4 42.1 82
Mixed xz (-mx5) 34.0 42.2 81
Tesseract NSIS solid LZMA (250 MB) 41.3 52.1 93

1.2–1.5× faster; from 2.3–3.1× slower than 7-Zip to 1.8–2.1×. Binary-heavy data gains less (frequent literal/match switches, far matches in a 32 MB window); several other layouts (all inline, match-only calls, cmov-style, branchy literals) were the same or slower.

Tests

  • New LzmaDecoderTests (10): 7 lc/lp/pb combinations, small dictionaries that wrap the window many times, streams fed 7 bytes at a time, random read sizes, truncated streams, LZMA/LZMA2 property variants made by 7-Zip.
  • Old and new decoders side by side for 400 s: 4,661 clean and 18,644 mutated inputs, identical output bytes and identical exception types and messages.
  • Related suites (Lzma, SevenZipReader/Writer, ArchiveFuzz, ArchiveInProcess, ArchiveTests): 207 passed, 1 skipped (benchmark), 0 failed. Fuzz 90 s for the xz, raw LZMA and 7z targets: 0 failures. NSIS tests (40, incl. the real Tesseract installer SHA-256 comparison) passed with these two files on the NSIS branch.
  • dotnet format --verify-no-changes --severity warn clean; no OS/architecture assumptions (Linux/macOS/ARM64 checked by CI).

Left

Getting to 2× needs cheaper literal↔match switches and fewer mispredicted decision bits. Archives/Crc32.cs (byte-table CRC) is the next biggest cost in 7z reading.

🤖 Generated with Claude Code

Rewrite the decoder's inner loop; output, errors and limits are unchanged.

- Decode into the window up to the end of the caller's buffer or of the
  window, then copy the block out, instead of writing every byte twice.
- Literal runs and match runs are decoded by two methods that keep the
  range coder, input position and state in locals for the whole run.
- All probabilities live in one array reached through bounded refs.
- Literal and tree bits are decoded with masks instead of branches;
  literals read both children's probabilities before the bit is known.
- While 64 or more input bytes are buffered (a symbol reads at most 48)
  bytes are read without checks; the rest of a chunk or stream is read
  by a checked copy of the same code. Stream input is topped up.
- Matches copy 8 bytes at a time when they don't overlap closely.

Same machine, median of 3, CPU MB/s old -> new (7-Zip 26.02 -mmt=1):
text .xz 24.6 -> 36.8 (77), mixed binary .xz 35.4 -> 42.1 (82),
Tesseract NSIS payload 41.3 -> 52.1 (93).

Tests: LzmaDecoderTests (all lc/lp/pb, wrapping dictionaries, odd read
and input sizes, truncation, 7-Zip LZMA/LZMA2 property variants).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KnifeLemon KnifeLemon added the built-in engines Filee: built-in engines (Project 4), on the builtin-engines branch, not released yet label Nov 14, 2026
@KnifeLemon
KnifeLemon merged commit d911d61 into builtin-engines Nov 14, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

built-in engines Filee: built-in engines (Project 4), on the builtin-engines branch, not released yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant