Skip to content

feat(encoding): long-distance matching for every strategy - #589

Merged
polaz merged 11 commits into
mainfrom
feat/#569-ldm-every-strategy
Oct 17, 2026
Merged

polaz merged 11 commits into
mainfrom
feat/#569-ldm-every-strategy

Conversation

@polaz

@polaz polaz commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Long-distance matching runs under every strategy, not only the optimal parsers, and reaches into a loaded dictionary.
  • Long matches stay whole across block cuts, so a patch against a large reference is a handful of sequences instead of thousands of one-sequence blocks.
  • The CLI takes --long at any level, turns long mode on for --patch-from by the upstream rule, and reads --memory as the decode window, as upstream does.

Changes

  • The long-distance producer moved from the optimal matcher to the driver; below btopt the block is matched in the pieces between accepted long matches.
  • A dictionary loaded with long mode on is indexed into the long-distance table; the short-range tables index only the part of a long dictionary their search can reach.
  • LdmSequenceStore keeps long matches between blocks; Matcher::settle_block_cut moves a splitter cut that falls inside one to its end.
  • A dictionary kept resident or restored from its primed snapshot is indexed into the cleared long-distance table each frame, as upstream fills it when it loads a dictionary; the snapshot holds no copy of that table.
  • CLI: no level floor for --long; --patch-from enables long mode when the file's window outgrows the match finder that runs (the level's, under any --zstd= override); -M/--memory sets the decode window ceiling.
  • README: the project relationship paragraph is replaced by an Origins section that keeps the ruzstd attribution.

Results

  • 61 MiB patch against a 60 MiB random reference, default level: about 5.4 KB (upstream: about 72 KB by default, 13 KB single-threaded); round-trips through both decoders.
  • Two copies of 140 MiB random data, --long=29: 146.8 MB at levels 1-15, smaller than upstream single-threaded at every level, task-clock within about 10% of it.
  • Without long mode the output is unchanged.

Testing

Unit and CLI tests, doc tests, clippy over the CI feature sets and formatting pass on macOS; every --long scenario of upstream tests/playTests.sh (v1.5.7) passes against the CLI.

Closes #569

Summary by CodeRabbit

  • New Features
    • Long-distance matching now works across compression strategies and levels, helping find distant repeats in input and dictionaries.
    • Long-distance matches are preserved across block boundaries in one-shot and streaming compression.
    • Patch compression selects long-distance matching based on the source data and active matching settings.
  • Bug Fixes
    • The -M decoding limit now applies directly as a ceiling on the decoding window, including when applying patches.
  • Documentation
    • Updated CLI guidance to reflect long-distance matching availability at every level.

polaz added 5 commits October 16, 2026 18:39
Long-distance matching only ran under the optimal parsers: the producer
lived on the binary-tree backend, so below btopt the setting was silently
ignored by every API, and the CLI refused `--long` under level 16.

- The match driver owns the long-distance matcher for the frame, as
  upstream keeps `ldmState` in the context. The optimal parser still
  takes its sequences as candidates; every other strategy takes them
  outright (upstream `ZSTD_ldm_blockCompress`): the block is committed,
  handed back and committed again in pieces, the backend matches each
  gap between matches as its own block, a match body is indexed only at
  its tail, and the repeat offsets move as upstream moves them.
- What a match may reach is fixed against the block end, one window
  back or the whole dictionary while the window has not passed it
  (`ZSTD_window_enforceMaxDist` on the chunk end).
- A dictionary loaded under long-distance matching is indexed into its
  table (`ZSTD_ldm_fillHashTable` from `ZSTD_loadDictionaryContent`); the
  primed snapshot keeps that table, keyed on its parameters, so a frame
  restored from it writes what a freshly primed one writes.
- A block is no longer written off by the raw-skip sample while the
  matcher is on: its matches are external to the block.
- CLI: `--long` runs at every level, and `--patch-from` turns long mode
  on by upstream's rule (`FIO_adjustParamsForPatchFromMode`:
  `fileWindowLog > ZSTD_cycleLog(chainLog, strategy)`).

A 61 MiB patch against a 60 MiB random reference at the default level
drops from 62.6 MB to 76 KB (upstream 72 KB) and round-trips through
both decoders.
A dictionary was indexed into the match-finder tables from its first
byte, however long it was. Tables of that size keep at most a window of
entries anyway, so the indexing of a large reference was pure cost:
priming a 60 MiB `--patch-from` reference at the default level indexed
all of it densely, a third of the run.

Upstream indexes only the end of a dictionary the tables can hold,
`MIN(MAX(hashLog + 3, chainLog + 1), 31)` bytes
(`ZSTD_loadDictionaryContent`, zstd_compress.c:4987), while the whole of
it stays in the window. The driver now sets that floor for the prime and
every backend's fill starts from it: the copied and attached tables of
the fast, double-fast, row, chain and tree finders, and the dictionary
match states.

A dictionary within the reach writes the same frames as before (levels
1-19 over three inputs and three dictionaries up to 125 KB, md5 equal).
A 930 KB dictionary moves a few frames by a handful of bytes, smaller in
all but one (+2 bytes on 449 KB). The 61 MiB patch against a 60 MiB
reference is byte-identical and takes 750 ms of task-clock instead of
1170 on the x86 bench host.
The block splitter reads byte statistics only. Over input that a long
match covers it still cuts, since a sparse sample of random bytes looks
different from the next one, and every cut costs a block whose one
sequence carries the same match on: a 61 MiB patch came out as 7605
blocks of a single sequence each.

- The long-distance matches are now searched ahead of the blocks and
  kept between them, as upstream keeps a raw sequence store across
  blocks: each block takes the matches covering it, the one crossing
  its end cut there and carried into the next, a piece shorter than the
  level's minMatch left as literals (`maybeSplitSequence`,
  `ZSTD_ldm_skipSequences`).
- Before a cut, both frame loops ask the matcher through the new
  `Matcher::settle_block_cut`; the driver searches the window the
  splitter looked at and moves a cut that falls inside a long match to
  the match's end.

The 61 MiB patch against a 60 MiB reference drops from 76127 bytes to
5386 (upstream: 71906 by default, 13163 single-threaded) and round-trips
through both decoders. Without long-distance matching nothing changes
(corpus totals at levels 1-19 equal). With `--long` the corpus comes out
smaller than upstream single-threaded `--long` at every level but 1,
where it is 0.02% larger.
Upstream sets the decoder's window limit to the -M value itself
(`ZSTD_DCtx_setMaxWindowSize(dctx, memLimit)` in fileio.c). We counted
the decoder's other buffers and the dictionary against the limit too,
so a frame with a 2^29 window was refused under `--memory=512MB`, and
a 2^28 one under `--memory=256MB`: both are decode cases of the
upstream `--long` test script.

- Under -M the window ceiling is the limit, clamped to the decoder's
  range; the dictionary no longer enters it.
- The `--memory` a refused frame names is the window rounded up, which
  now decodes it; the benchmark keeps its own accounting.
Replace the fork paragraph with an Origins section: the project is
developed on its own, and the ruzstd code it started from stays
credited to its author under the MIT License, whose notice is kept in
NOTICE.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T22:28:25.699967Z db566fa New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: structured-world/structured-zstd/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 4dd274e1-a6b6-4c5b-b5fc-1c50de93017f
📥 Commits

Reviewing files that changed from the base of the PR and between b489c78 and db566fa.

📒 Files selected for processing: 5
  • zstd/src/bin/structured-zstd/main.rs
  • zstd/src/bin/structured-zstd/tests.rs
  • zstd/src/encoding/ldm/mod.rs
  • zstd/src/encoding/levels/config.rs
  • zstd/src/encoding/levels/config/tests.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: structured-world/structured-zstd/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 2b2b8072-9989-447e-ac28-48b15011c8c2
📥 Commits

Reviewing files that changed from the base of the PR and between 3780628 and b489c78.

📒 Files selected for processing: 9
  • zstd/src/bin/structured-zstd/main.rs
  • zstd/src/bin/structured-zstd/tests.rs
  • zstd/src/encoding/ldm/mod.rs
  • zstd/src/encoding/ldm/table.rs
  • zstd/src/encoding/match_generator/dict_prime.rs
  • zstd/src/encoding/match_generator/long_distance.rs
  • zstd/src/encoding/match_generator/mod.rs
  • zstd/src/encoding/match_generator/tests.rs
  • zstd/src/tests/parameters_test.rs
💤 Files with no reviewable changes: 2
  • zstd/src/encoding/ldm/table.rs
  • zstd/src/encoding/ldm/mod.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.

📝 Walkthrough

The encoder now supports long-distance matching across strategies, including dictionary content, and settles block cuts around known matches. The CLI updates --long and patch-mode behavior. Decoder -M limits now apply directly to the window.

Changes: Long-distance matching

Layer / File(s) Summary
LDM producer and sequence store
zstd/src/encoding/ldm/*, zstd/src/encoding/match_generator/long_distance.rs
The LDM producer can fill its hash table from history. A sequence store tracks matches across blocks and adjusts cuts inside matches. Tests cover extraction and carry-over.
Dictionary reach and indexing
zstd/src/encoding/levels/config.rs, zstd/src/encoding/match_generator/dict_prime.rs, zstd/src/encoding/match_table/storage.rs, zstd/src/encoding/{dfast,row,simple}/*
The encoder derives dictionary reach and minimum match lengths from the active strategy. Matchers apply an indexing floor during dictionary priming, and dictionary restore paths re-index the dictionary.
Driver and strategy integration
zstd/src/encoding/match_generator/*, zstd/src/encoding/{bt,hc}/*, zstd/src/encoding/{dfast,row,simple}/*, zstd/src/encoding/levels/fastest.rs, zstd/src/tests/parameters_test.rs
The driver owns LDM state and routes matches through optimal and non-optimal paths. Backend helpers support history access and long-match bookkeeping. Tests cover strategies, dictionaries, streaming, and reused contexts.
Block-cut settlement
zstd/src/encoding/mod.rs, zstd/src/encoding/frame_compressor.rs, zstd/src/encoding/streaming_encoder.rs
Matcher hooks settle proposed cuts within pending input. Owned and streaming compression use the hook.
CLI activation and LDM validation
README.md, zstd/src/bin/structured-zstd/main.rs, zstd/src/bin/structured-zstd/tests.rs, zstd/src/encoding/parameters.rs, zstd/src/encoding/match_generator/tests.rs
The CLI accepts --long at all levels and enables it for patch input when the source window exceeds match-finder reach. Tests cover thresholds, overrides, and lower-level long-distance compression.

Decoder window limits

Layer / File(s) Summary
Window ceiling and decode settings
zstd/src/bin/structured-zstd/main.rs, zstd/src/bin/structured-zstd/tests.rs
-M sets the decode window ceiling directly. Ordinary decode settings use zero memory overhead, and tests update window and memory-guidance expectations.

Project origins documentation

Layer / File(s) Summary
Origins section
README.md
The README replaces the project-relationship section with an Origins section.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant MatchGeneratorDriver
  participant LdmProducer
  participant LdmSequenceStore
  participant Matcher
  MatchGeneratorDriver->>LdmProducer: Search available history for long-distance matches
  LdmProducer-->>MatchGeneratorDriver: Return found sequences
  MatchGeneratorDriver->>LdmSequenceStore: Record and take sequences for the block
  MatchGeneratorDriver->>Matcher: Process literal gaps and long-match bookkeeping
Loading
Merge Risk: ⚪ Minimal · up to b489c

No identified issue currently blocks merging. Normal validation remains appropriate.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Out of Scope Changes check Warning The PR changes the independent decoder --memory contract. decode_window_ceiling now treats the memory limit as the full decode window and removes dictionary and auxiliary-buffer accounting in `zst… Remove the --memory contract changes and their tests, and remove the unrelated README Origins change, or link those changes to separate issues. Keep the LDM implementation, patch activation, dictionary indexing, and required validation.
Docstring Coverage Warning Docstring coverage is 78.72% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 141 functions across 23 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely identifies the main change: enabling long-distance matching for every compression strategy.
Linked Issues check Passed Issue #569 requires LDM for every strategy and levels 1–15, dictionary indexing, automatic LDM for large --patch-from inputs, and the listed round-trip and upstream test coverage. The current change…
Full details: Linked Issues check

Explanation

Issue #569 requires LDM for every strategy and levels 1–15, dictionary indexing, automatic LDM for large --patch-from inputs, and the listed round-trip and upstream test coverage. The current changes move LDM state to MatchGeneratorDriver, process non-optimal strategies, retain matches across block cuts, fill LDM from dictionaries, and apply patch_needs_long_distance using effective matcher parameters. The PR summary reports the required patch-size result, low-level repeat result, decoder round-trips, and all upstream v1.5.7 --long scenarios.

Full details: Out of Scope Changes check

Explanation

The PR changes the independent decoder --memory contract. decode_window_ceiling now treats the memory limit as the full decode window and removes dictionary and auxiliary-buffer accounting in zstd/src/bin/structured-zstd/main.rs. Issue #569 does not request this decoder behavior. The PR also replaces the README project-relationship paragraph with an unrelated Origins section.

Resolution

Remove the --memory contract changes and their tests, and remove the unrelated README Origins change, or link those changes to separate issues. Keep the LDM implementation, patch activation, dictionary indexing, and required validation.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 378062819b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zstd/src/encoding/match_generator/mod.rs
Comment thread zstd/src/bin/structured-zstd/main.rs
Comment thread zstd/src/encoding/match_generator/dict_prime.rs Outdated
polaz added 4 commits October 17, 2026 00:28
`--patch-from` turned long mode on from the bare level's chain and
strategy, while a `--zstd=` override changes the matcher that actually
runs. A short `clog=` at a high level left the reference out of reach
with long mode off, and the patch came out as literals. The cycle check
now reads the overridden chain and strategy. The reference command reads
the bare level here; following the effective matcher only differs where
its patch would miss the reference.
- A dictionary a reused context keeps resident between small or unsized
  frames had its long-distance table cleared by the reset and never
  filled again, so every frame after the first lost its long matches
  into the dictionary. The resident path now indexes the dictionary
  into the cleared table, as priming it would; a test runs three frames
  on one context against a fresh encoder under every strategy.
- The primed snapshot no longer keeps a copy of the long-distance
  table: the restore path indexes the restored dictionary instead, as
  upstream fills the table each frame it loads a dictionary
  (`ZSTD_ldm_fillHashTable`). The context held two copies of its
  largest table, which the workspace estimates did not count, and
  copying it cost more than the fill. With no long-distance state in
  the snapshot its key drops the matcher's parameters, and a restore
  across a long-distance switch writes what a fresh compressor writes.
- Gaps between long matches, borrowed scans and plain blocks report
  through one sink type, so each backend's scan is compiled once per
  caller instead of twice. The library's LLVM IR had grown by 64%;
  it is back within 0.3% of main, and the test build on CI's macOS
  runner had tripled.
The sink every owned match runs through forwarded each report as the
matched value whole, which the compiler copies as one 32-byte block out
of the separate stores that wrote its fields. Rebuilding the report from
its fields brings the encoder's instruction count back to that of the
code before the shared sink (387.13M at level 9 on z000033, from
387.51M), with output identical at every level.
The hash-chain lazy search runs only under the parse and search
override, so its long-distance path (handing the block back, skipping
the match body, carrying the offset into the repeat history) had no
test. The new one checks that a shuffled repeat beyond the chain's reach
stays literals without long-distance matching and is matched with it,
and that the stream rebuilds both ways.
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

let ldm = overrides.ldm.map_or(0, |ldm| {
let ldm_params = frame_ldm_params(&params, &ldm);
crate::encoding::ldm::table::LdmHashTable::estimated_workspace_bytes(
ldm_params.hash_log,
ldm_params.bucket_size_log,

P2 Badge Budget the retained LDM sequence vectors

When LDM produces many short matches—especially with a custom ldmMinMatch=4 and a low hash-rate log—this estimate adds only the hash table, while the driver also retains capacities for ldm_found, ldm_block, and LdmSequenceStore::matches (and the optimal backend can retain another candidate vector). A 128 KiB block can therefore leave several MiB of unbudgeted sequence storage, so the CLI's benchmark -M check can approve a limit that the compression context exceeds; include worst-case LDM sequence-store capacity in both workspace estimators.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zstd/src/bin/structured-zstd/main.rs Outdated
polaz added 2 commits October 17, 2026 01:22
A benchmark with `--patch-from` compresses every level from `-b` to
`-e`, but long mode was decided on the first level alone. Whether a
level needs it is not constant across a range: at an equal chain log
btlazy2 (level 13) cannot cycle the input where lazy2 (level 12) can,
so `-b12 -e13` measured level 13 without long mode, as literals. Long
mode now turns on when any benchmarked level needs it; the regression
test runs that range.
Both workspace estimates counted the long-distance table alone, while
the driver also keeps the sequences it searched, its store, the block's
sequences and the optimal parser's copy of them. At a short minimum
match those hold several MiB, so a benchmark `-M` limit could pass a
run that then exceeds it. The estimates now add five vectors of a
block's worth of sequences at the minimum match, upstream's per-store
bound (`ZSTD_ldm_getMaxNbSeq`); a test checks that minMatch 4 budgets
at least one such vector more than minMatch 64.
@polaz
polaz merged commit e9067ad into main Oct 17, 2026
29 checks passed
@polaz
polaz deleted the feat/#569-ldm-every-strategy branch October 17, 2026 07:11
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.

feat(encoding): long-distance matching for every strategy and over the dictionary

1 participant