Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions docs/design-docs/clean-slate-codec.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,14 +409,26 @@ segment tier earning its place in the format.
copies split). Naive per-block priming is ruled out by arithmetic:
94 blocks × 16 MiB ≈ 1.5 GB of memcpy ≈ +15-30 ms — would double the
16.8 ms decode wall.
- **Encode (the real blocker):** the spike re-tokenizes dict+block per
block (9× work at 16 MiB — fine for measurement, unshippable). Production
needs a **frozen shared match-finder**: build hash chains over the dict
once (immutable, `Arc`-shared head/prev arrays in dict coordinates),
each worker holds a `dict‖block` arena (dict copied once per worker, so
frozen-table coordinates match and compares never need two-region
logic) and parses starting at `dict_len` — which also eliminates the
spike's token-skipping/straddle handling. `find_best` grows one extra
chain walk over the frozen tables after the block-local walk.
- **Measured payoff (per-segment, 16 MiB dict):** blob 31.04% → **30.48%**,
~0.9pp under pzstd-3 (31.4%), at unchanged decode parallelism.
- **Encode: the frozen shared match-finder is BUILT and measured**
(`lz77::FrozenDict` + `lzseq::tokenize_with_dict` +
`pz2::encode_with_frozen_dict`): dict chains built once (immutable,
`Arc`-shared, dict-relative coordinates), each worker holds a
`dict‖block` arena (so frozen coordinates match and compares read one
buffer) and parses starting at `dict_len` — no dict re-parse, no
token-skipping/straddle handling. `find_best` grows one extra chain
walk over the frozen tables after the block-local walk, sharing the
chain budget. Probe `--frozen` (per-segment, blob): **identical ratio
to the re-parse spike (30.475%) at 3× its encode speed** (70.5 s vs
208.8 s ST). Remaining encode cost is the dict chain walks themselves
(5.3× the no-dict baseline at 16 MiB) — a 32-byte weak-local-match
gate was measured and rejected (−8% time, +0.018pp: most text
positions have weak local matches, so the walk is inherent). Tuning
levers for integration: dict-specific chain caps, sampled dict
insertion, 4-8 MiB dicts (4 MiB: −0.31pp at 33.7 s).
- **Measured payoff (per-segment, 16 MiB dict, frozen finder):** blob
31.04% → **30.48%**, ~0.9pp under pzstd-3 (31.4%), at unchanged decode
parallelism.
- **What remains for shipping `Pz2d`:** container integration only —
segment framing (`[dict_len] +` blocks), 2-wave parallel decode with
the `Arc`-shared dict + worker arenas, encode-side worker arenas, and
the encode-cost tuning pass above.
42 changes: 40 additions & 2 deletions examples/pz2_dict_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ fn main() {
} else {
false
};
// --frozen: per-segment head dict via the production frozen finder
// (FrozenDict built once per segment, parse starts at the dict
// boundary) instead of the spike's full dict+block re-parse. Same wire,
// same decoder; encode cost is the point.
let frozen_mode = if let Some(i) = args.iter().position(|a| a == "--frozen") {
args.remove(i);
true
} else {
false
};
const SEG: usize = 32 << 20;
if args.is_empty() {
eprintln!("usage: pz2_dict_probe [--head] <files...>");
Expand All @@ -53,7 +63,9 @@ fn main() {
"ratio %",
"delta",
"enc s",
if seg_mode {
if frozen_mode {
"per-segment FROZEN finder"
} else if seg_mode {
"per-segment head dict"
} else if head_mode {
"fixed head dict"
Expand All @@ -76,9 +88,35 @@ fn main() {
let t = Instant::now();
let mut size = 0usize;
let mut start = 0usize;
// Frozen mode: build each segment's dict tables ONCE (the
// production shape — Arc-shared across workers).
let mut frozen: Option<(usize, std::sync::Arc<pz::lz77::FrozenDict>, usize)> = None;
while start < data.len() {
let end = (start + BLOCK).min(data.len());
let (enc, prefix): (Vec<u8>, &[u8]) = if head_mode || seg_mode {
let (enc, prefix): (Vec<u8>, &[u8]) = if frozen_mode {
let seg_base = start - (start % SEG);
let dlen = d.min(start - seg_base);
let dict = &data[seg_base..seg_base + dlen];
let reuse = matches!(frozen, Some((b, _, l)) if b == seg_base && l == dlen);
if !reuse {
frozen = Some((
seg_base,
std::sync::Arc::new(pz::lz77::FrozenDict::build(
dict,
config.hash_prefix_len,
)),
dlen,
));
}
let tables = &frozen.as_ref().unwrap().1;
let mut arena = Vec::with_capacity(dlen + (end - start));
arena.extend_from_slice(dict);
arena.extend_from_slice(&data[start..end]);
(
pz::pz2::encode_with_frozen_dict(&arena, tables, &config).expect("encode"),
dict,
)
} else if head_mode || seg_mode {
// Dict = first min(d, start - base) bytes of the file
// (--head) or of the block's 32 MiB segment (--seg);
// blocks inside the dict region parse cold (they ARE
Expand Down
136 changes: 136 additions & 0 deletions src/lz77/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,70 @@ pub(crate) struct WideMatch {
///
/// Maintains a hash table mapping 3-byte or 4-byte prefixes to positions,
/// with chains for collision resolution. Average O(n) complexity.
/// Immutable hash-chain tables over a dictionary prefix, built once and
/// shared read-only (`Arc`) by many parallel block parses (the pz2 dict
/// tier's frozen finder).
///
/// Positions are dict-relative (`0..len`). The parse input handed to
/// [`HashChainFinder::find_match_wide`] MUST carry the same dict bytes at
/// those positions (i.e. `input = dict ‖ block`), so frozen coordinates need
/// no translation and match compares read one contiguous buffer. The walk
/// in `find_best` additionally guards `pos >= dict.len`, so a misuse cannot
/// read out of bounds — it just finds nothing useful.
pub struct FrozenDict {
/// head[hash] = most recent dict position with this hash (0 = empty or
/// position 0 — the same benign ambiguity the live finder has).
head: Vec<u32>,
/// prev[pos] = previous dict position in the chain, indexed directly.
prev: Vec<u32>,
len: usize,
hash_prefix_len: u8,
}

impl FrozenDict {
/// Build chains over `dict` once. Cost is one insert per position
/// (no chain walks), ~hundreds of MB/s; share the result via `Arc`.
pub fn build(dict: &[u8], hash_prefix_len: u8) -> Self {
let mut head = vec![0u32; HASH_SIZE];
let mut prev = vec![0u32; dict.len()];
// Same lookahead guard as `insert`: the last 2 positions can't hash.
let hashable = dict.len().saturating_sub(2);
for (pos, slot) in prev.iter_mut().enumerate().take(hashable) {
let h = if hash_prefix_len == 4 {
hash4(dict, pos)
} else {
hash3(dict, pos)
};
*slot = head[h];
head[h] = pos as u32;
}
FrozenDict {
head,
prev,
len: dict.len(),
hash_prefix_len,
}
}

/// Dictionary length in bytes.
pub fn len(&self) -> usize {
self.len
}

/// Whether the dictionary is empty.
pub fn is_empty(&self) -> bool {
self.len == 0
}
}

pub(crate) struct HashChainFinder {
/// head[hash] = most recent position with this hash, or 0
head: Vec<u32>,
/// prev[pos % max_window] = previous position in the chain
prev: Vec<u32>,
/// Optional frozen dictionary chains consulted after the live chain
/// (pz2 dict tier). See [`FrozenDict`] for the coordinate contract.
dict: Option<std::sync::Arc<FrozenDict>>,
/// Cached SIMD dispatcher — resolved once, avoids per-call feature detection.
dispatcher: crate::simd::Dispatcher,
/// Maximum match length to find. Deflate pipelines use 258 (RFC 1951);
Expand Down Expand Up @@ -272,6 +331,7 @@ impl HashChainFinder {
Self {
head: vec![0; HASH_SIZE],
prev: vec![0; MAX_WINDOW],
dict: None,
dispatcher: crate::simd::Dispatcher::new(),
max_match_len: max_match_len as usize,
max_chain: max_chain.clamp(1, MAX_CHAIN * 4),
Expand All @@ -297,6 +357,7 @@ impl HashChainFinder {
Self {
head: vec![0; HASH_SIZE],
prev: vec![0; max_window],
dict: None,
dispatcher: crate::simd::Dispatcher::new(),
max_match_len: max_match_len as usize,
max_chain: MAX_CHAIN,
Expand All @@ -319,6 +380,7 @@ impl HashChainFinder {
Self {
head: vec![0; HASH_SIZE],
prev: vec![0; max_window],
dict: None,
dispatcher: crate::simd::Dispatcher::new(),
max_match_len: max_match_len as usize,
max_chain: max_chain.clamp(1, MAX_CHAIN * 4),
Expand All @@ -340,6 +402,7 @@ impl HashChainFinder {
Self {
head: vec![0; HASH_SIZE],
prev: vec![0; max_window],
dict: None,
dispatcher: crate::simd::Dispatcher::new(),
max_match_len: max_match_len as usize,
max_chain: max_chain.clamp(1, MAX_CHAIN * 4),
Expand All @@ -359,6 +422,17 @@ impl HashChainFinder {
}
}

/// Install frozen dictionary chains, consulted by `find_best` after the
/// live chain. The parse input must carry the dict bytes as its prefix
/// (see [`FrozenDict`]).
pub(crate) fn set_frozen_dict(&mut self, dict: std::sync::Arc<FrozenDict>) {
debug_assert_eq!(
dict.hash_prefix_len, self.hash_prefix_len,
"frozen dict and finder must hash identically"
);
self.dict = Some(dict);
}

/// Dynamically adjust chain depth. Used by adaptive encoding loops.
/// Clamps to 1..=MAX_CHAIN*4.
pub(crate) fn set_max_chain(&mut self, max_chain: usize) {
Expand Down Expand Up @@ -439,6 +513,68 @@ impl HashChainFinder {
chain_count += 1;
}

// Frozen dictionary chains (pz2 dict tier): consulted after the live
// chain — recency first, dictionary as fallback — sharing the same
// chain budget. (A weak-local-match gate at 32 bytes was measured:
// it saves only ~8% encode for +0.018pp on the blob, because most
// positions on text HAVE weak local matches — the walk cost is
// inherent. Tune via dict-specific chain caps at integration time
// if needed.) The `pos >= dict.len` guard makes every read below
// in-bounds regardless of caller behavior: chain_pos < dict.len ≤
// pos, so chain_pos + probe/cmp_limit < pos + remaining =
// input.len().
if let Some(dict) = self.dict.as_deref() {
if pos >= dict.len && (best_length as usize) < cmp_limit && chain_count < self.max_chain
{
let mut chain_pos = dict.head[h] as usize;
while chain_pos < dict.len && chain_pos >= min_pos && chain_count < self.max_chain {
if best_length >= MIN_MATCH as u32 {
let probe = best_length as usize;
// SAFETY: chain_pos < dict.len <= pos and probe <
// remaining (see block comment above).
let candidate_probe = unsafe { *input_ptr.add(chain_pos + probe) };
if candidate_probe != best_probe_byte {
let prev_pos = dict.prev[chain_pos] as usize;
if prev_pos >= chain_pos || prev_pos < min_pos {
break;
}
chain_pos = prev_pos;
chain_count += 1;
continue;
}
}

// SAFETY: chain_pos + cmp_limit < pos + remaining (block
// comment above); candidates may extend past the dict end
// into the block — valid under the primed-buffer contract.
let match_len = unsafe {
self.dispatcher.compare_bytes_ptr(
input_ptr.add(chain_pos),
pos_ptr,
cmp_limit,
)
} as u32;

if match_len > best_length && match_len >= MIN_MATCH as u32 {
best_length = match_len;
best_offset = (pos - chain_pos) as u32;
if best_length as usize >= cmp_limit {
break;
}
// SAFETY: best_length < cmp_limit <= remaining.
best_probe_byte = unsafe { *input_ptr.add(pos + best_length as usize) };
}

let prev_pos = dict.prev[chain_pos] as usize;
if prev_pos >= chain_pos || prev_pos < min_pos {
break;
}
chain_pos = prev_pos;
chain_count += 1;
}
}
}

(best_offset, best_length)
}

Expand Down
22 changes: 20 additions & 2 deletions src/lzseq/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -983,11 +983,25 @@ pub fn encode_with_config(input: &[u8], config: &SeqConfig) -> PzResult<SeqEncod
pub(crate) fn tokenize_with_config(
input: &[u8],
config: &SeqConfig,
) -> PzResult<Vec<crate::lz_token::LzToken>> {
tokenize_with_dict(input, 0, None, config)
}

/// [`tokenize_with_config`] generalized for the pz2 dict tier: tokens are
/// emitted for `input[start..]` only, and an optional frozen dictionary
/// (whose bytes must be `input[..dict.len()]`, with `start >= dict.len()`)
/// extends the match finder's reach without re-inserting the dict — the
/// frozen chains were built once and are shared read-only across workers.
pub(crate) fn tokenize_with_dict(
input: &[u8],
start: usize,
dict: Option<std::sync::Arc<crate::lz77::FrozenDict>>,
config: &SeqConfig,
) -> PzResult<Vec<crate::lz_token::LzToken>> {
use crate::lz_token::LzToken;

let mut tokens: Vec<LzToken> = Vec::new();
if input.is_empty() {
if input.len() <= start {
return Ok(tokens);
}

Expand All @@ -997,8 +1011,12 @@ pub(crate) fn tokenize_with_config(
} else {
HashChainFinder::with_window_and_chain(config.max_window, match_limit, config.max_chain)
};
if let Some(d) = dict {
debug_assert!(start >= d.len(), "parse must start at/after the dict end");
finder.set_frozen_dict(d);
}
let mut repeats = RepeatOffsets::new();
let mut pos: usize = 0;
let mut pos: usize = start;
let max_match_len = match_limit as usize;

// Adaptive chain depth tracking — identical constants to encode_with_config.
Expand Down
Loading
Loading