Context
Surfaced during review of #166 (FSE_buildCTable_wksp parity / drop per-symbol Vec<State>). Part of #111 Phase 7 Lane A — block compressor entropy coding speed.
Profile of compress/level_2_dfast/small-4k-log-lines/matrix/pure_rust (post-#166):
| % inclusive |
Function |
| ~32 % |
huff0_encoder::HuffmanTable::try_table_description_size |
| ~23 % |
huff0_encoder::HuffmanTable::build_from_counts |
| ~18 % |
huff0_encoder::HuffmanEncoder::encode_weight_description |
Problem
HuffmanTable::build_from_counts runs a speculative search over min_table_log..=11, calling try_table_description_size on each candidate. try_table_description_size actually FSE-encodes the weights to a fresh buffer — this is the expensive operation. The loop pays 7× FSE-encode-of-weights to pick the optimal (desc_size + payload_size) tableLog.
Donor's HUF_optimalTableLog (lib/compress/huf_compress.c:1272) has two modes:
- Default (fast) — pure arithmetic via
FSE_optimalTableLog_internal(maxLog=11, srcSize, maxSV, minus=1). Single tableLog, no per-candidate rebuild.
HUF_flags_optimalDepth — full search loop. Gated by strategy >= ZSTD_btultra (level 18+).
For all strategies below btultra (every dfast/lazy level in this codebase's hot path) donor uses the fast path. Rust currently does the slow search unconditionally.
What was tried and why it was reverted
Implemented the donor fast path inside #166 work (Stage 1). Result on compress/level_2_dfast/small-4k-log-lines/matrix/pure_rust:
Per project rule "Ratio first — if rust_bytes > ffi_bytes we lose vs donor → real bug", the single-shot change cannot land while it produces a rust_bytes > ffi_bytes cell. Stage 1 reverted; only Stage 2 (PR #166) landed.
Suggested approach
The expensive component of the current loop is try_table_description_size, which FSE-encodes weights to count bytes. Replace that with a cheap description-size proxy that approximates the FSE-encoded weight description size from a histogram of the per-symbol weights (no actual FSE encode). The proxy should:
- Compute the histogram of weight values (13 bins, donor max weight = 12).
- Approximate the FSE-compressed weight description size analytically — e.g. via
H(weights) * total_weights / 8 plus a small constant for the FSE header. Possibly compose with a min-of-raw-encoding (weights.len().div_ceil(2) + 1) since the donor picks the cheaper of FSE / raw.
- Validate that the proxy's relative ranking across
tableLog candidates matches try_table_description_size ranking on the bench corpus (small-4k-log-lines, decodecorpus-z000033, large-log-stream, high-entropy-1m).
- Verify ratio is at least as good as the current search loop vs the donor on every level × scenario cell (compare_ffi REPORT sweep).
If the proxy preserves the optimum within tolerance, the loop becomes:
for table_log in min_table_log..=11 {
let weights = build_donor_limited_weights(counts, table_log);
if !huffman_weight_sum_is_power_of_two(&weights) { continue; }
let table = Self::build_from_weights(&weights);
let max_bits = table.codes.iter().map(|&(_, b)| b).max().unwrap_or(0) as usize;
if max_bits < table_log && table_log > min_table_log { break; }
let desc_size_est = huff_desc_size_proxy(&weights); // <- cheap
let new_size = table.estimate_compressed_size_from_counts(counts) + desc_size_est;
if new_size < best_size { best_size = new_size; best_table = Some(table); }
if new_size > best_size + 1 { break; }
}
— same selection shape, no FSE-encode per candidate. Expected speedup matches the Stage 1 measurement (~−22 % on small-4k-log L2_dfast on top of #166), with no ratio regression vs donor since the proxy still drives the same selection criterion.
Files involved
zstd/src/huff0/huff0_encoder.rs — HuffmanTable::build_from_counts (~line 220), HuffmanEncoder::encode_weight_description (~line 125), new proxy fn.
zstd/benches/compare_ffi.rs — REPORT ratio sweep is the validation gate.
Acceptance criteria
Related
Context
Surfaced during review of #166 (
FSE_buildCTable_wkspparity / drop per-symbolVec<State>). Part of #111 Phase 7 Lane A — block compressor entropy coding speed.Profile of
compress/level_2_dfast/small-4k-log-lines/matrix/pure_rust(post-#166):huff0_encoder::HuffmanTable::try_table_description_sizehuff0_encoder::HuffmanTable::build_from_countshuff0_encoder::HuffmanEncoder::encode_weight_descriptionProblem
HuffmanTable::build_from_countsruns a speculative search overmin_table_log..=11, callingtry_table_description_sizeon each candidate.try_table_description_sizeactually FSE-encodes the weights to a fresh buffer — this is the expensive operation. The loop pays 7× FSE-encode-of-weights to pick the optimal(desc_size + payload_size)tableLog.Donor's
HUF_optimalTableLog(lib/compress/huf_compress.c:1272) has two modes:FSE_optimalTableLog_internal(maxLog=11, srcSize, maxSV, minus=1). SingletableLog, no per-candidate rebuild.HUF_flags_optimalDepth— full search loop. Gated bystrategy >= ZSTD_btultra(level 18+).For all strategies below btultra (every dfast/lazy level in this codebase's hot path) donor uses the fast path. Rust currently does the slow search unconditionally.
What was tried and why it was reverted
Implemented the donor fast path inside #166 work (Stage 1). Result on
compress/level_2_dfast/small-4k-log-lines/matrix/pure_rust:small-4k-log-lines/L1_fastrust_bytes=159vsffi_bytes=157(+2 B). Pre-perf(fse): donor FSE_buildCTable_wksp parity — drop per-symbol Vec<State> #166 search loop producedR=154(better than donor); single-shot loses that 5 B margin and falls 2 B below the donor.Per project rule "Ratio first — if
rust_bytes > ffi_byteswe lose vs donor → real bug", the single-shot change cannot land while it produces arust_bytes > ffi_bytescell. Stage 1 reverted; only Stage 2 (PR #166) landed.Suggested approach
The expensive component of the current loop is
try_table_description_size, which FSE-encodes weights to count bytes. Replace that with a cheap description-size proxy that approximates the FSE-encoded weight description size from a histogram of the per-symbol weights (no actual FSE encode). The proxy should:H(weights) * total_weights / 8plus a small constant for the FSE header. Possibly compose with a min-of-raw-encoding (weights.len().div_ceil(2) + 1) since the donor picks the cheaper of FSE / raw.tableLogcandidates matchestry_table_description_sizeranking on the bench corpus (small-4k-log-lines, decodecorpus-z000033, large-log-stream, high-entropy-1m).If the proxy preserves the optimum within tolerance, the loop becomes:
— same selection shape, no FSE-encode per candidate. Expected speedup matches the Stage 1 measurement (~−22 % on small-4k-log L2_dfast on top of #166), with no ratio regression vs donor since the proxy still drives the same selection criterion.
Files involved
zstd/src/huff0/huff0_encoder.rs—HuffmanTable::build_from_counts(~line 220),HuffmanEncoder::encode_weight_description(~line 125), new proxy fn.zstd/benches/compare_ffi.rs—REPORTratio sweep is the validation gate.Acceptance criteria
huff0_encoder.rswithdebug_assertcross-checks againsttry_table_description_sizeon a representative sample (e.g. property test).compare_ffi --features dict_builderREPORT sweep: zero new cells whererust_bytes > ffi_bytescompared to current main.compress/level_2_dfast/small-4k-log-lines/matrix/pure_rustimproves at least −10 % vs current main (post-perf(fse): donor FSE_buildCTable_wksp parity — drop per-symbol Vec<State> #166 47.4 µs baseline).Related