Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
ca4617f
Add LZ4HC, LZ4OPT & LZ4MID compression support
yujincheng08 Mar 28, 2026
36be3c0
Rename variables in compress_hc.rs to match compress.rs conventions
PSeitz Mar 28, 2026
41d819c
Extract try_ext_dict_match helper to deduplicate ext_dict matching
PSeitz Mar 28, 2026
63804a9
Flatten nesting in HashTableHCU32 search methods
PSeitz Mar 28, 2026
68b032e
refactor: simplify compress_hc_internal match selection with labeled …
PSeitz Mar 28, 2026
1632b4e
refactor: extract inner fns from compress_mid_internal to methods/fun…
PSeitz Mar 28, 2026
04da7ba
refactor: tighten visibility and remove debug test in compress_hc
PSeitz Mar 28, 2026
aeba8ad
refactor
PSeitz Mar 28, 2026
c55e28e
clippy
PSeitz Mar 28, 2026
c56539a
add comment
PSeitz Mar 28, 2026
94f1afa
cleanup, refactor
PSeitz Mar 28, 2026
ba15c51
renames
PSeitz Mar 29, 2026
dbde976
refactor
PSeitz Mar 29, 2026
e11ea04
align variable naming
PSeitz Mar 29, 2026
449c4b8
refactor
PSeitz Mar 29, 2026
a1bee2e
replace loop, add benchmark
PSeitz Mar 30, 2026
6a61ee4
flatten branches
PSeitz Apr 1, 2026
f25e109
align naming conventions
PSeitz Apr 4, 2026
3f975c5
align naming conventions
PSeitz Apr 5, 2026
b3d2ca5
Rename reference_position to candidate in Match struct
PSeitz Apr 5, 2026
8c92728
add exact test
PSeitz Apr 5, 2026
6188e6c
cleanup bench
PSeitz Apr 5, 2026
4fec1be
Rename hash-chain compressor and bump binggan
PSeitz Apr 5, 2026
37c9799
cleanup hashtable
PSeitz Apr 5, 2026
6d52d3f
Add level 2 to hc benchmark
PSeitz Apr 7, 2026
5326e3f
refactor
PSeitz Apr 5, 2026
c63f480
refactor
PSeitz Apr 5, 2026
6c6e75f
refactor structure
PSeitz Apr 6, 2026
6ebcf28
move hc to seperate files
PSeitz Apr 6, 2026
bead0b1
Clarify lz4mid 8-byte hash endianness
PSeitz Apr 6, 2026
013652b
Clarify lz4mid match start and end
PSeitz Apr 6, 2026
8964448
Extract lz4mid match finder
PSeitz Apr 7, 2026
295dbcb
add missing inline
PSeitz Apr 7, 2026
9e8c74a
add reuse hashtable bench
PSeitz Apr 7, 2026
bf54190
precheck for collisions
PSeitz Apr 8, 2026
b9a52b9
remove struct
PSeitz Apr 8, 2026
7edd342
add comment
PSeitz Apr 9, 2026
ecefbdc
add block format description
PSeitz Apr 13, 2026
c93e821
add USE_DICT in mid.rs
PSeitz Apr 18, 2026
715368a
ignore bench files in rg
PSeitz Apr 19, 2026
7de2d2f
rename mid to two-hashtables
PSeitz Apr 25, 2026
2cd4070
better docs
PSeitz Apr 26, 2026
d8d32b4
Refactor HC hash-chain match helpers
PSeitz May 16, 2026
5739b5b
Improve LZ4HC compression ratio by removing prehash
PSeitz May 16, 2026
dcb58ec
Update binggan benchmark dependency
PSeitz May 16, 2026
5b52063
attribution
PSeitz May 16, 2026
d651d72
ignore test files
PSeitz May 16, 2026
5562534
remove duplicated code
PSeitz May 17, 2026
d841efc
remove unnecessary code
PSeitz May 17, 2026
2d8b32c
introduce CandidateSource
PSeitz May 17, 2026
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
7 changes: 7 additions & 0 deletions .rgignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Ignore benchmark corpus data in ripgrep results.
# Keep Rust benchmark source files searchable.
benches/**
!benches/**/*.rs

# Ignore downloaded benchmark corpus files, if present.
benchmarks/bench_files/**
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Code style

- No abbreviations in variable names (`match_length` not `ml`, `forward_len` not `fwd`)
- No static methods — use freestanding functions or instance methods
- No logic blocks inside boolean expressions — extract a function instead
- Variable names must be self-explanatory without context (`cur` not `off`)
- Name variables the way you would describe them — if you'd say "can't beat the best match", name it `cant_beat_best`, not `dominated`
- No meaningless suffixes or qualifiers — every word in a name must carry information (e.g. don't say "local" when there is no non-local counterpart)
- Align naming with compress.rs conventions (`cur`, `candidate`, `literal_start`, `match_limit`)
- Extract repeated patterns into named functions
- Flatten nesting: prefer early returns/continues over deep `if` blocks — use guard clauses instead of `if { value } else { 0 }` patterns
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ twox-hash = { version = "2.0.0", default-features = false, features = ["xxhash32
codegen-units = 1
lto = true
opt-level = 3
debug = true
Comment thread
PSeitz marked this conversation as resolved.

[profile.release]
codegen-units = 1
Expand Down
98 changes: 0 additions & 98 deletions benches/bench.rs

This file was deleted.

51 changes: 50 additions & 1 deletion benches/binggan_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ fn main() {
frame_compress(InputGroup::new_with_inputs(data_sets));
}

let named_data = ALL
let named_data: Vec<_> = ALL
.iter()
.map(|data| (data.len().to_string(), data.to_vec()))
.collect();
block_compress(InputGroup::new_with_inputs(named_data));
block_compress_hc();
block_decompress();
}

Expand Down Expand Up @@ -144,6 +145,54 @@ fn block_compress(mut runner: InputGroup<Vec<u8>, usize>) {
runner.run();
}

fn lz4_cpp_block_compress_hc(input: &[u8], level: i32) -> Result<Vec<u8>, lzzzz::Error> {
let mut out = Vec::new();
lzzzz::lz4_hc::compress_to_vec(input, &mut out, level)?;
Ok(out)
}

fn block_compress_hc() {
let mut runner = BenchRunner::with_name("block_compress_hc");
runner.add_plugin(PeakMemAllocPlugin::new(&GLOBAL));

for data in ALL {
let mut group = runner.new_group();
group.set_name(format!("{}", data.len()));
group.set_input_size(data.len());

for level in [2u8, 3u8, 5, 9, 12] {
group.register_with_input(format!("lz4_flex_level_{level}"), data, move |i| {
let out = black_box(lz4_flex::block::compress_hc_to_vec(i, level));
out.len()
});
{
let table = std::cell::RefCell::new(lz4_flex::block::CompressTableHC::new());
// Note that the lz4 c90 reference implementation has a thread local reuse, so this
// is a more accurate comparison
group.register_with_input(
format!("lz4_flex_level_{level}_reuse"),
data,
move |i| {
let out = black_box(lz4_flex::block::compress_hc_to_vec_with_table(
i,
level,
&mut table.borrow_mut(),
));
out.len()
},
);
}
if level >= 2 {
group.register_with_input(format!("lz4_c90_level_{level}"), data, move |i| {
let out = black_box(lz4_cpp_block_compress_hc(i, level as i32).unwrap());
out.len()
});
}
}
group.run();
}
}

fn block_decompress() {
let mut runner = BenchRunner::with_name("block_decompress");
// Set the peak mem allocator. This will enable peak memory reporting.
Expand Down
177 changes: 177 additions & 0 deletions docs/compress_hc_algorithms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# `compress_hc.rs` high-level algorithm overview

This file contains **three different high-compression strategies** behind the same public API.

At a high level, all three produce normal LZ4 sequences:

- a run of **literals**
- followed by a **match**
- an `offset`
- a `match length`

What changes between the strategies is **how hard they search for matches** and **how they choose between competing matches**.

## Strategy selection

`compress_hc()` first clamps the level and maps it to one of these strategies:

- **levels 0-2**: `TwoHashTables`
- **levels 3-9**: `HashChain`
- **levels 10-12**: `Optimal`

That mapping is defined by `hc_level_params()`.

---

## 1. Two-hashtables strategy

**Used for levels 0-2**

### Core idea

This is the lightweight HC mode.

It keeps two plain hash tables:

- one keyed by **4-byte sequences**
- one keyed by **8-byte sequences**

Each table stores only the **most recent** position for a hash.

So unlike the full hash-chain HC mode, it does **not** keep a long linked history of older candidates.

### How it works

At each position:

1. Try the **8-byte hash** first
2. If that does not produce a usable match, try the **4-byte hash**
3. If a match is found:
- extend it backward if possible
- extend it forward as far as bytes keep matching
- emit it immediately
4. If no match is found, move forward with a small acceleration rule

There is also a small amount of local lookahead around the 4-byte path, but the strategy is still mostly greedy.

---

## 2. Hash-chain HC strategy

**Used for levels 3-9**

### Core idea

Hash-chain HC stores previous positions for each 4-byte hash:

- `dictionary[hash]`: newest position with that hash
- `chain_table[position % chain_table.len()]`: delta to the previous position with that hash

```rust
previous_position = position - chain_table[position % chain_table.len()]
```

Only positions within the LZ4 maximum match distance are usable. Each search
checks at most `max_attempts` chain links.

### How it works

At each position:

1. Insert the current position into the dictionary and chain table.
2. Look up the current 4-byte hash in the dictionary to get the newest candidate.
3. Walk backward through the chain, stopping at `max_attempts` or when the candidate is too old.
4. For each candidate, check whether it really matches and keep the best match found at this position.
5. Before emitting, search again near the end of that match, at `match.end() - 2`, for a better follow-up match.
6. If the matches overlap, trim or shorten them so the emitted LZ4 sequences stay valid, then emit.

For levels 3-9, `max_attempts = 1 << (level - 1)`.

---

## 3. Optimal parsing strategy

**Used for levels 10-12**

### Core idea

This mode still uses strong hash-chain-based match finding, but the decision rule changes completely.

Instead of asking:

> What is the best match right now?

it asks:

> What sequence of literals and matches gives the cheapest encoded output over the next window?

So this is a small **dynamic-programming parser**.

### How it works

At a position:

1. Find a strong initial match
2. If that match is obviously good enough, emit it immediately as a fast path
3. Otherwise, build an optimal parse window
4. For each reachable position in that window, track the best known encoding cost to get there
5. Refine those costs by exploring additional matches from intermediate positions
6. Reverse the winning path and emit the chosen sequence of literals and matches

The DP state is stored in the `OptimalState` array.

### Cost model

The parser compares the byte cost of choices using helper functions such as:

- `literals_price()`
- `sequence_price()`

So it is explicitly choosing the path with the best compressed size inside the lookahead window.

### Level differences

Within optimal mode:

- **level 10**: moderate search
- **level 11**: deeper search
- **level 12**: most exhaustive update/search behavior

### Decision style

This mode is:

- the **slowest** of the three
- the **best ratio** of the three
- the only one doing explicit **cost-based path optimization**

### Mental model

> Draft several possible futures, score them by encoded size, and choose the cheapest path.

---

## Quick comparison

| Strategy | Levels | Match search | Match choice | Speed | Ratio |
|---|---:|---|---|---|---|
| TwoHashTables | 0-2 | Very shallow | Greedy/local | Fastest of the three | Lowest of the three |
| Hash-chain HC | 3-9 | Deep chain walk | Local + lazy overlap resolution | Middle | Better |
| Optimal | 10-12 | Deep chain walk | Dynamic programming over a window | Slowest | Best |

---

## How to read `compress_hc.rs`

If you want to read the file top-down, this is the main story:

1. `compress_hc()`
- chooses the strategy from the level
2. `compress_two_hash_tables_internal()`
- two-hash-tables, mostly greedy compression
3. `compress_hash_chain_internal()`
- hash-chain search plus lazy local match resolution
4. `compress_opt_internal()`
- optimal parsing with a dynamic-programming window

That is the main conceptual split in the file.
18 changes: 18 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,21 @@ name = "fuzz_decomp_no_output_leak"
path = "fuzz_targets/fuzz_decomp_no_output_leak.rs"
test = false
doc = false

[[bin]]
name = "fuzz_roundtrip_hc"
path = "fuzz_targets/fuzz_roundtrip_hc.rs"
test = false
doc = false

[[bin]]
name = "fuzz_roundtrip_hc_cpp"
path = "fuzz_targets/fuzz_roundtrip_hc_cpp.rs"
test = false
doc = false

[[bin]]
name = "fuzz_roundtrip_hc_frame"
path = "fuzz_targets/fuzz_roundtrip_hc_frame.rs"
test = false
doc = false
Loading
Loading