use std::time::Instant;
use fastcdc::v2020::FastCDC;
use mothcdc::MothChunker;
use mothcdc::mincdc::{MinCdcHash4, SliceChunker};
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
h
}
#[derive(Default)]
struct Stats {
records: usize,
logical: usize,
gbps: f64,
}
struct Dedup {
seen: std::collections::HashMap<u64, usize>,
logical: u64,
records: usize,
}
impl Dedup {
fn new() -> Self {
Self {
seen: std::collections::HashMap::new(),
logical: 0,
records: 0,
}
}
fn add(&mut self, key: u64, stored_len: usize, logical_len: u64) {
self.seen.entry(key).or_insert(stored_len);
self.logical += logical_len;
self.records += 1;
}
fn unique(&self) -> usize {
self.seen.len()
}
fn dedup_pct(&self) -> f64 {
let stored: usize = self.seen.values().sum();
100.0 * (1.0 - stored as f64 / self.logical.max(1) as f64)
}
}
fn xorshift(seed: u64, n: usize) -> Vec<u8> {
let mut s = seed | 1;
(0..n)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
(s >> 33) as u8
})
.collect()
}
fn texty(seed: u64, n: usize) -> Vec<u8> {
let words: [&[u8]; 16] = [
b"the", b"quick", b"brown", b"fox", b"jumps", b"over", b"lazy", b"dog", b"and", b"then",
b"runs", b"away", b"into", b"dark", b"deep", b"woods",
];
let mut s = seed | 1;
let mut out = Vec::with_capacity(n + 16);
while out.len() < n {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
out.extend_from_slice(words[(s as usize) & 15]);
out.push(b' ');
}
out.truncate(n);
out
}
const ITERS: usize = 3;
fn bench<F: FnMut() -> usize>(bytes_len: usize, mut produce: F) -> (f64, usize) {
let mut best = f64::MAX;
let mut records = 0;
for _ in 0..ITERS {
let t = Instant::now();
records = produce();
let e = t.elapsed().as_secs_f64();
best = best.min(e);
}
let gbps = bytes_len as f64 / best / 1e9;
(gbps, records)
}
fn run_fastcdc(data: &[u8], min: usize, avg: usize, max: usize, d: &mut Dedup) -> Stats {
let (gbps, records) = bench(data.len(), || {
let mut n = 0;
for c in FastCDC::new(data, min, avg, max) {
std::hint::black_box(c.offset);
n += 1;
}
n
});
for c in FastCDC::new(data, min, avg, max) {
let b = &data[c.offset..c.offset + c.length];
d.add(fnv1a(b), b.len(), b.len() as u64);
}
Stats {
records,
logical: data.len(),
gbps,
}
}
fn run_mincdc(data: &[u8], min: usize, max: usize, d: &mut Dedup) -> Stats {
let cdc = MinCdcHash4::new();
let (gbps, records) = bench(data.len(), || {
let mut n = 0;
for c in SliceChunker::new(data, min, max, cdc) {
std::hint::black_box(c.offset());
n += 1;
}
n
});
for c in SliceChunker::new(data, min, max, cdc) {
d.add(fnv1a(&c), c.len(), c.len() as u64);
}
Stats {
records,
logical: data.len(),
gbps,
}
}
fn run_caterpillar(data: &[u8], min: usize, max: usize, d: &mut Dedup) -> Stats {
let cdc = MinCdcHash4::new();
let make = || MothChunker::with_cdc(data, min, max, cdc);
let (gbps, records) = bench(data.len(), || {
let mut n = 0;
for s in make() {
std::hint::black_box(s.offset());
n += 1;
}
n
});
for s in make() {
d.add(fnv1a(s.dedup_key()), s.dedup_key().len(), s.len());
}
Stats {
records,
logical: data.len(),
gbps,
}
}
fn row(name: &str, s: &Stats, d: &Dedup) {
let mean = s.logical.checked_div(s.records).unwrap_or(0);
println!(
" {name:<18} {gbps:>7.2} GB/s records={rec:>7} mean={mean:>6} uniq={uniq:>7} dedup={dd:>5.1}%",
gbps = s.gbps,
rec = s.records,
uniq = d.unique(),
dd = d.dedup_pct(),
);
}
fn scenario_versioned(min: usize, avg: usize, max: usize) {
let v1 = xorshift(2024, 8 * 1024 * 1024);
let mut v2 = Vec::with_capacity(v1.len() + 5000);
let cut = v1.len() / 2;
v2.extend_from_slice(&v1[..cut]);
v2.extend_from_slice(&xorshift(999, 5000)); v2.extend_from_slice(&v1[cut..]);
let total = v1.len() + v2.len();
println!(
"\n=== versioned (v1 + v2-with-insert) ({} MiB total) ===",
total / (1024 * 1024)
);
let mut d = Dedup::new();
for data in [&v1[..], &v2[..]] {
for c in FastCDC::new(data, min, avg, max) {
let b = &data[c.offset..c.offset + c.length];
d.add(fnv1a(b), b.len(), b.len() as u64);
}
}
println!(
" {:<18} records={:>7} uniq={:>7} dedup={:>5.1}%",
"fastcdc-v2020",
d.records,
d.unique(),
d.dedup_pct()
);
let cdc = MinCdcHash4::new();
let mut d = Dedup::new();
for data in [&v1[..], &v2[..]] {
for c in SliceChunker::new(data, min, max, cdc) {
d.add(fnv1a(&c), c.len(), c.len() as u64);
}
}
println!(
" {:<18} records={:>7} uniq={:>7} dedup={:>5.1}%",
"mincdc-plain",
d.records,
d.unique(),
d.dedup_pct()
);
let mut d = Dedup::new();
for data in [&v1[..], &v2[..]] {
for s in MothChunker::with_cdc(data, min, max, cdc) {
d.add(fnv1a(s.dedup_key()), s.dedup_key().len(), s.len());
}
}
println!(
" {:<18} records={:>7} uniq={:>7} dedup={:>5.1}%",
"mincdc+cat",
d.records,
d.unique(),
d.dedup_pct()
);
}
fn main() {
let (min, avg, max) = (2048usize, 8192usize, 14336usize);
let fast_max = avg + (avg - min) * 7; let n = 16 * 1024 * 1024;
run_suite(min, avg, max, fast_max, n);
}
fn run_suite(min: usize, avg: usize, mc_max: usize, fast_max: usize, n: usize) {
let block = |title: &str, data: &[u8]| {
println!("\n=== {title} ({} MiB) ===", data.len() / (1024 * 1024));
let mut d = Dedup::new();
let s = run_fastcdc(data, min, avg, fast_max, &mut d);
row("fastcdc-v2020", &s, &d);
let mut d = Dedup::new();
let s = run_mincdc(data, min, mc_max, &mut d);
row("mincdc-plain", &s, &d);
let mut d = Dedup::new();
let s = run_caterpillar(data, min, mc_max, &mut d);
row("mincdc+cat", &s, &d);
};
block("random / incompressible", &xorshift(1, n));
block("zero-fill", &vec![0u8; n]);
block("english-ish text", &texty(5, n));
{
let p = xorshift(42, 777);
let mut data = Vec::with_capacity(n);
while data.len() < n {
data.extend_from_slice(&p);
}
block("periodic-777 (wide win)", &data);
}
{
let p = xorshift(42, 777);
let mut data = Vec::with_capacity(n);
while data.len() < n {
data.extend_from_slice(&p);
}
let (nmin, nmax) = (2048usize, 2200usize);
println!(
"\n=== periodic-777 (NARROW win min={nmin} max={nmax}) ({} MiB) ===",
data.len() / (1024 * 1024)
);
let mut d = Dedup::new();
let s = run_fastcdc(&data, nmin, 2100, nmax, &mut d);
row("fastcdc-v2020", &s, &d);
let mut d = Dedup::new();
let s = run_mincdc(&data, nmin, nmax, &mut d);
row("mincdc-plain", &s, &d);
let mut d = Dedup::new();
let s = run_caterpillar(&data, nmin, nmax, &mut d);
row("mincdc+cat", &s, &d);
}
{
let mut data = xorshift(7, n);
for b in data.iter_mut().take(n / 2).skip(n / 4) {
*b = 0;
}
let rec = xorshift(3, 333);
let start = n - n / 8;
let mut i = start;
while i + rec.len() <= n {
data[i..i + rec.len()].copy_from_slice(&rec);
i += rec.len();
}
block("mixed (random+holes+recs)", &data);
}
scenario_versioned(min, avg, fast_max);
}