Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1b3df02
Checkpoint experiments
fintelia Sep 21, 2024
ef3b099
checkpoint
fintelia Sep 21, 2024
2d25d48
checkpoint
fintelia Sep 21, 2024
d06cd69
Checkpoint
fintelia Sep 21, 2024
17d3d58
Fixes
fintelia Sep 21, 2024
484eaf5
Actually produce valid output
fintelia Sep 22, 2024
1cccbc6
Checkpoint hash chain compression
fintelia Nov 21, 2024
5279f21
More lazy
fintelia Nov 21, 2024
161a2bb
Split blocks based on number of symbols seen
fintelia Nov 21, 2024
8c4e806
Adjust parameters and a bugfix
fintelia Nov 21, 2024
d9ec3b3
Hash on demand
fintelia Nov 22, 2024
c77d39d
Hash collision fixes and a few other improvements
fintelia Nov 23, 2024
a5a51f6
Optimizations and experiments
fintelia Nov 24, 2024
ee7b1bc
Segment detection
fintelia Nov 24, 2024
5fa54e2
checkpoint
fintelia Nov 27, 2024
c63e91b
get_and_insert and fewer constants
fintelia Dec 8, 2024
d2a0387
Fix lazy matching
fintelia Dec 8, 2024
1f9dde0
Return length/distance rather than index/length
fintelia Dec 8, 2024
1b2a95a
Pass through parameters
fintelia Dec 8, 2024
16aff78
Fix warnings
fintelia Dec 8, 2024
377fc29
Better lazy parsing
fintelia Dec 9, 2024
2a4a540
Independent hash3_table and hash4_table handling
fintelia Dec 9, 2024
a2e40af
Improvements
fintelia Dec 10, 2024
0bcb092
Optimize literal writing
fintelia Dec 11, 2024
d777fda
Checkpoint
fintelia Dec 28, 2024
1b92038
Checkpoint refactoring
fintelia Jan 8, 2025
1c22667
Add missing file
fintelia Jan 9, 2025
bce8419
Checkpoint bt matchfinding
fintelia Jan 13, 2025
e6858b3
Checkpoint BST fixes
fintelia Jan 13, 2025
821bd9f
Checkpoint high compression fixes
fintelia Jan 14, 2025
34ba136
Support longer min_match
fintelia Jun 28, 2025
8574a72
Tune for faster compression
fintelia Jun 28, 2025
7c5bb5e
Checkpoint hashtable match finder
fintelia Jun 28, 2025
6357744
fast compressor improvements
fintelia Jun 28, 2025
067caaf
Look for RLE during skipahead
fintelia Jun 28, 2025
fdb9d5e
Change hash function
fintelia Jun 28, 2025
8eb50d7
Merge branch 'main' into compression-levels
fintelia Jul 2, 2025
cf2391d
Refactor interface and initial calibration
fintelia Jul 3, 2025
126f131
minmatch=4 for now
fintelia Jul 3, 2025
0a527a9
Non-overlapping "lazy" matching
fintelia Jul 11, 2025
f9fb91e
Match zlib-ng more closely for 'medium' compression levels
fintelia Jul 11, 2025
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ homepage = "https://github.com/image-rs/fdeflate"
categories = ["compression"]

[dependencies]
fnv = "1.0.7"
innumerable = "0.1.0"
simd-adler32 = "0.3.4"

[dev-dependencies]
Expand Down
203 changes: 203 additions & 0 deletions src/compress/bt_matchfinder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use super::{compute_hash, compute_hash3, WINDOW_SIZE};

const CACHE3_SIZE: usize = 1 << 15;
const CACHE_SIZE: usize = 1 << 16;

/// Find the length of the match between the current position and the previous position, searching
/// both forwards and backwards from the starting position.
fn match_length(data: &[u8], ip: usize, prev_index: usize) -> u16 {
assert!(
prev_index < ip,
"Match past current position: {prev_index} {ip}"
);

let mut length = 0;
while length < 258 && ip + length < data.len() && data[ip + length] == data[prev_index + length]
{
length += 1;
}
length as u16
}

fn left_child(index: usize) -> usize {
2 * (index as usize % WINDOW_SIZE)
}

fn right_child(index: usize) -> usize {
2 * (index as usize % WINDOW_SIZE) + 1
}

/// Match finder that uses a binary tree to find matches.
///
/// Based on bt_matchfinder.h from libdeflate.
pub(crate) struct BTreeMatchFinder {
hash3_table: Option<Box<[u32; CACHE3_SIZE]>>,
hash_table: Box<[u32; CACHE_SIZE]>,
child_links: Box<[u32; WINDOW_SIZE * 2]>,
search_depth: u16,
early_return_length: usize,
}
impl BTreeMatchFinder {
pub(crate) fn new(min_match: u8) -> Self {
assert!((3..=4).contains(&min_match));

Self {
hash3_table: (min_match == 3)
.then(|| vec![0; CACHE3_SIZE].into_boxed_slice().try_into().unwrap()),
hash_table: vec![0; CACHE_SIZE].into_boxed_slice().try_into().unwrap(),
child_links: vec![0; WINDOW_SIZE * 2]
.into_boxed_slice()
.try_into()
.unwrap(),
search_depth: 2000,
early_return_length: 256,
}
}

fn update(
&mut self,
data: &[u8],
ip: usize,
value: u64,
min_match: u16,
record_matches: bool,
) -> (u16, u16, usize) {
let min_offset = ip.saturating_sub(WINDOW_SIZE).max(1);

let mut best_offset = 0;
let mut best_length = min_match - 1;

// Handle 3-byte matches
if let Some(hash3_table) = &mut self.hash3_table {
let hash3 = compute_hash3(value as u32);
if best_length < min_match && min_match <= 3 {
let hash3_offset = hash3_table[(hash3 as usize) % CACHE3_SIZE] as usize;
if hash3_offset >= ip.saturating_sub(8192).max(1) {
let length = match_length(data, ip, hash3_offset);
if length >= 3 {
best_length = length;
best_offset = hash3_offset as u32;
}
}
}
hash3_table[(hash3 as usize) % CACHE3_SIZE] = ip as u32;
}

// Lookup current value
let hash = compute_hash(value & 0xffff_ffff);
let hash_index = (hash as usize) % CACHE_SIZE;
let mut offset = self.hash_table[hash_index] as usize;
self.hash_table[hash_index] = ip as u32;

let mut pending_left = left_child(ip);
let mut pending_right = right_child(ip);

if offset < min_offset {
self.child_links[pending_left] = 0;
self.child_links[pending_right] = 0;
return (0, 0, ip);
}

let mut best_left_length = 0;
let mut best_right_length = 0;
let mut length = 0;

// Visit previous matches
// eprintln!("---");
let mut depth_remaining = self.search_depth;
loop {
if data[ip + length] == data[offset + length] {
while length < 258
&& ip + length < data.len()
&& data[ip + length] == data[offset + length]
{
length += 1;
}

// for i in 0..length.min(self.early_return_length) {
// assert_eq!(
// data[ip + i],
// data[offset + i],
// "{i} {length} ip={ip} data_len={}",
// data.len()
// );
// }

if record_matches && length > best_length as usize {
best_length = length as u16;
best_offset = offset as u32;
}

if length >= self.early_return_length || ip + length == data.len() {
self.child_links[pending_left] = self.child_links[left_child(offset)];
self.child_links[pending_right] = self.child_links[right_child(offset)];
break;
}
}

assert!(ip + length < data.len());

if data[offset + length] < data[ip + length] {
self.child_links[pending_left] = offset as u32;
pending_left = right_child(offset);
offset = self.child_links[pending_left] as usize;

best_left_length = length;
if best_right_length < length {
length = best_right_length;
}
// length = length.min(best_right_length);
// eprintln!(
// "left {best_right_length},{best_left_length} dist={}",
// ip - offset
// );
} else {
assert!(
data[offset + length] > data[ip + length],
"{length} {depth_remaining} {offset} {min_offset}"
);

self.child_links[pending_right] = offset as u32;
pending_right = left_child(offset);
offset = self.child_links[pending_right] as usize;

best_right_length = length;
if best_left_length < length {
length = best_left_length;
}
// length = length.min(best_left_length);
// eprintln!(
// "right {best_right_length},{best_left_length} dist={}",
// ip - offset
// );
}

depth_remaining -= 1;
if offset <= min_offset || depth_remaining == 0 {
self.child_links[pending_left] = 0;
self.child_links[pending_right] = 0;
break;
}
}

if best_length >= min_match {
return (best_length as u16, (ip - best_offset as usize) as u16, ip);
}

(0, 0, ip)
}

pub(crate) fn get_and_insert(
&mut self,
data: &[u8],
ip: usize,
value: u64,
min_match: u16,
) -> (u16, u16, usize) {
self.update(data, ip, value, min_match, true)
}

pub(crate) fn insert(&mut self, data: &[u8], value: u64, ip: usize) {
self.update(data, ip, value, 3, false);
}
}
120 changes: 120 additions & 0 deletions src/compress/fast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::io::{self, Write};

use super::{BitWriter, HashTableMatchFinder, Symbol};

pub(super) struct FastCompressor {
match_finder: HashTableMatchFinder,
skip_ahead_shift: u8,
}

impl FastCompressor {
pub fn new(skip_ahead_shift: u8) -> Self {
Self {
match_finder: HashTableMatchFinder::new(),
skip_ahead_shift,
}
}

pub fn compress<W: Write>(&mut self, writer: &mut BitWriter<W>, data: &[u8]) -> io::Result<()> {
let mut ip = 0;

while ip < data.len() {
let mut symbols = Vec::new();

let mut last_match = ip;
'outer: while symbols.len() < 16384 && ip + 8 <= data.len() {
let current = u64::from_le_bytes(data[ip..][..8].try_into().unwrap());

if current & 0xFF_FFFF_FFFF == 0 {
while ip > last_match && data[ip - 1] == 0 {
ip -= 1;
}

if ip == 0 || data[ip - 1] != 0 {
ip += 1;
}

symbols.push(Symbol::LiteralRun {
start: last_match as u32,
end: ip as u32,
});

let mut run_length = 0;
while ip < data.len() && data[ip] == 0 && run_length < 258 {
run_length += 1;
ip += 1;
}

symbols.push(Symbol::Backref {
length: run_length as u16,
distance: 1,
dist_sym: 0,
});

last_match = ip;

continue;
}

let (length, distance, match_start) = self
.match_finder
.get_and_insert(&data, last_match, ip, current, 4);

if length >= 3 {
assert!(last_match <= match_start);

symbols.push(Symbol::LiteralRun {
start: last_match as u32,
end: match_start as u32,
});

symbols.push(Symbol::Backref {
length: length as u16,
distance,
dist_sym: super::distance_to_dist_sym(distance),
});

let match_end = match_start + length as usize;
let insert_end = (match_end - 2).min(data.len() - 8);
let insert_start = (ip + 1).max(insert_end.saturating_sub(16));
for j in insert_start..insert_end {
let v = u64::from_le_bytes(data[j..][..8].try_into().unwrap());
self.match_finder.insert(v, j);
}

ip = match_end;
last_match = ip;

continue 'outer;
}

// If we haven't found a match in a while, start skipping ahead by emitting multiple
// literals at once. But check that we don't skip over a big run of zeroes.
let advance = 1 + ((ip - last_match) >> self.skip_ahead_shift);
if advance >= 8 {
let end_index = (ip + advance).min(data.len());
if let Some(advance) = data[ip + 1..end_index]
.chunks_exact(8)
.position(|w| w == [0; 8])
{
ip += advance + 1;
continue 'outer;
}
}

ip += advance;
}
if data.len() < ip + 8 {
symbols.push(Symbol::LiteralRun {
start: last_match as u32,
end: data.len() as u32,
});
ip = data.len();
}

super::write_block(writer, data, &symbols, ip == data.len())?;
}

Ok(())
}
}
Loading
Loading