use std::time::{Duration, Instant};
use vecdb::{
AnyStoredVec, BytesVec, Database, ImportableVec, LZ4Vec, PcoVec, ReadableVec, StoredVec,
Version, WritableVec, ZeroCopyVec, ZstdVec,
};
const DEFAULT_VALUE_COUNT: usize = 10_000_000_000; const BATCH_SIZE: usize = 10_000_000;
const MAX_RANGE_BYTES: usize = 8 * 1024 * 1024 * 1024;
fn value_count() -> usize {
std::env::var("BENCH_COUNT")
.ok()
.and_then(|s| s.replace('_', "").parse().ok())
.unwrap_or(DEFAULT_VALUE_COUNT)
}
fn range_passes() -> usize {
std::env::var("BENCH_PASSES")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(5)
}
fn range_sizes(count: usize, value_size: usize) -> Vec<usize> {
let max_elements = (MAX_RANGE_BYTES / value_size).min(count);
let mut sizes = Vec::new();
let mut n = 1_000;
while n <= max_elements {
sizes.push(n);
let next5 = n * 5;
if next5 <= max_elements && next5 != n * 10 {
sizes.push(next5);
}
n *= 10;
}
sizes
}
fn repetitions(range_size: usize) -> usize {
match range_size {
n if n < 10_000 => 5_000,
n if n < 100_000 => 1_000,
n if n < 1_000_000 => 100,
n if n < 10_000_000 => 20,
n if n < 100_000_000 => 5,
_ => 1,
}
}
fn xorshift(state: &mut u64) -> u64 {
*state ^= *state << 13;
*state ^= *state >> 7;
*state ^= *state << 17;
*state
}
fn random_starts(count: usize, max_start: usize) -> Vec<usize> {
let mut rng = 42u64;
(0..count)
.map(|_| xorshift(&mut rng) as usize % max_start.max(1))
.collect()
}
fn format_bytes(bytes: usize) -> String {
if bytes >= 1_000_000_000 {
format!("{:.1} GB", bytes as f64 / 1e9)
} else if bytes >= 1_000_000 {
format!("{:.1} MB", bytes as f64 / 1e6)
} else if bytes >= 1_000 {
format!("{:.1} KB", bytes as f64 / 1e3)
} else {
format!("{} B", bytes)
}
}
fn format_duration(d: Duration) -> String {
let ns = d.as_nanos();
if ns >= 1_000_000_000 {
format!("{:.2} s", d.as_secs_f64())
} else if ns >= 1_000_000 {
format!("{:.2} ms", ns as f64 / 1e6)
} else if ns >= 1_000 {
format!("{:.2} us", ns as f64 / 1e3)
} else {
format!("{} ns", ns)
}
}
fn throughput_str(bytes: usize, d: Duration) -> String {
format!("{:.1} GB/s", bytes as f64 / d.as_secs_f64() / 1e9)
}
fn drop_caches() -> bool {
#[cfg(target_os = "macos")]
{
std::process::Command::new("purge")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(target_os = "linux")]
{
std::process::Command::new("sync").status().ok();
std::fs::write("/proc/sys/vm/drop_caches", "3").is_ok()
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
false
}
}
fn check_cache_eviction() -> bool {
let ok = drop_caches();
if ok {
println!(" Cache eviction: available (cold-cache benchmarks)");
} else {
println!(" Cache eviction: unavailable (warm-cache benchmarks)");
println!(" Hint: run with sudo for cold-cache results");
}
ok
}
fn print_vec_size(vec: &dyn AnyStoredVec, label: &str) {
let region_bytes = vec.region().meta().len();
let logical_bytes = vec.len() * vec.value_type_to_size_of();
let ratio = if logical_bytes > 0 {
region_bytes as f64 / logical_bytes as f64
} else {
0.0
};
eprintln!(
" {label} on disk: {} (logical: {}, ratio: {:.2}x)",
format_bytes(region_bytes),
format_bytes(logical_bytes),
ratio,
);
}
fn populate<V: WritableVec<usize, u64> + ImportableVec + AnyStoredVec>(
db: &Database,
label: &str,
count: usize,
) -> V {
eprint!(" Populating {label} with {count} values...");
flush();
let start = Instant::now();
let mut vec: V = V::import(db, "bench", Version::ONE).unwrap();
let mut written = 0;
while written < count {
let end = (written + BATCH_SIZE).min(count);
for i in written..end {
vec.push(i as u64);
}
vec.write().unwrap();
written = end;
eprint!(
"\r Populating {label}: {:.0}% ",
written as f64 / count as f64 * 100.0
);
}
db.flush().unwrap();
eprintln!("\r Populated {label} ({:?}) ", start.elapsed());
print_vec_size(&vec, label);
vec
}
struct BenchResult {
name: &'static str,
duration: Duration,
}
fn print_full_results(results: &[BenchResult], total_bytes: usize) {
let best = results.iter().min_by_key(|r| r.duration).unwrap().duration;
for r in results {
let pct = if r.duration > best {
let overhead = (r.duration.as_secs_f64() / best.as_secs_f64() - 1.0) * 100.0;
format!("(+{overhead:.0}%)")
} else {
String::new()
};
println!(
" {:<20} {} ({}) {}",
r.name,
format_duration(r.duration),
throughput_str(total_bytes, r.duration),
pct,
);
}
}
fn print_range_header(columns: &[&str]) {
print!("{:>12} {:>10}", "range", "bytes");
for col in columns {
print!(" {:>14}", col);
}
println!(" {:<8}", "winner");
let width = 24 + columns.len() * 15 + 10;
println!("{}", "-".repeat(width));
}
fn print_range_row(range_size: usize, results: &[BenchResult]) {
let range_bytes = range_size * 8;
print!("{:>12} {:>10}", range_size, format_bytes(range_bytes));
let best = results.iter().min_by_key(|r| r.duration).unwrap();
for r in results {
print!(" {:>14}", format_duration(r.duration));
}
println!(" {:<8}", best.name);
}
trait StoredFold {
fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64;
fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64;
}
impl StoredFold for BytesVec<usize, u64> {
fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
}
impl StoredFold for ZeroCopyVec<usize, u64> {
fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
}
impl StoredFold for LZ4Vec<usize, u64> {
fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
}
impl StoredFold for PcoVec<usize, u64> {
fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
}
impl StoredFold for ZstdVec<usize, u64> {
fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
}
}
fn bench_vec<
V: ReadableVec<usize, u64> + StoredFold + AnyStoredVec + StoredVec<I = usize, T = u64>,
>(
vec: &V,
label: &str,
count: usize,
can_purge: bool,
) {
let total_bytes = count * 8;
let disk_bytes = vec.region().meta().len();
let ratio = if total_bytes > 0 {
disk_bytes as f64 / total_bytes as f64
} else {
0.0
};
let ranges = range_sizes(count, 8);
println!(
"\n=== {label} — {count} values ({}, disk: {}, {:.2}x) ===\n",
format_bytes(total_bytes),
format_bytes(disk_bytes),
ratio,
);
let full_passes = range_passes();
println!("--- Full scan ({full_passes} passes, random order) ---");
let method_names: [&str; 6] = [
"fold_stored_io",
"fold_stored_mmap",
"fold_range",
"try_fold_range",
"for_each_dyn",
"for_each",
];
let mut times = [Duration::ZERO; 6];
let mut sum = 0u64;
let mut rng = 0xCAFEu64;
for _pass in 0..full_passes {
let mut order = [0usize, 1, 2, 3, 4, 5];
for i in (1..6).rev() {
let j = xorshift(&mut rng) as usize % (i + 1);
order.swap(i, j);
}
for &method in &order {
if can_purge {
drop_caches();
}
let t = Instant::now();
match method {
0 => sum = vec.fold_stored_io_sum(0, count, sum),
1 => sum = vec.fold_stored_mmap_sum(0, count, sum),
2 => sum = vec.fold_range(0, count, sum, |a, v: u64| a.wrapping_add(v)),
3 => {
sum = vec
.try_fold_range(0, count, sum, |a, v: u64| Ok::<_, ()>(a.wrapping_add(v)))
.unwrap()
}
4 => vec.for_each_range_dyn(0, count, &mut |v: u64| sum = sum.wrapping_add(v)),
5 => vec.for_each_range(0, count, |v: u64| sum = sum.wrapping_add(v)),
_ => unreachable!(),
}
times[method] += t.elapsed();
}
}
std::hint::black_box(sum);
let results: Vec<BenchResult> = (0..6)
.map(|i| BenchResult {
name: method_names[i],
duration: times[i] / full_passes as u32,
})
.collect();
print_full_results(&results, total_bytes);
let passes = range_passes();
println!("\n--- Range scans ({passes} passes, random order) ---");
let columns: Vec<&str> = vec!["IO", "Mmap", "fold", "try_fold", "dyn", "static"];
print_range_header(&columns);
for &range_size in &ranges {
let reps = repetitions(range_size);
let max_start = count.saturating_sub(range_size);
let starts = random_starts(reps, max_start);
let mut times = [Duration::ZERO; 6];
let mut sum = 0u64;
let mut rng = range_size as u64 ^ 0xDEAD;
for _pass in 0..passes {
let mut order = [0usize, 1, 2, 3, 4, 5];
for i in (1..6).rev() {
let j = xorshift(&mut rng) as usize % (i + 1);
order.swap(i, j);
}
for &method in &order {
if can_purge {
drop_caches();
}
let t = Instant::now();
for &s in &starts {
let from = s;
let to = s + range_size;
match method {
0 => sum = vec.fold_stored_io_sum(from, to, sum),
1 => sum = vec.fold_stored_mmap_sum(from, to, sum),
2 => sum = vec.fold_range(from, to, sum, |a, v: u64| a.wrapping_add(v)),
3 => {
sum = vec
.try_fold_range(from, to, sum, |a, v: u64| {
Ok::<_, ()>(a.wrapping_add(v))
})
.unwrap()
}
4 => vec
.for_each_range_dyn(from, to, &mut |v: u64| sum = sum.wrapping_add(v)),
5 => vec.for_each_range(from, to, |v: u64| sum = sum.wrapping_add(v)),
_ => unreachable!(),
}
}
times[method] += t.elapsed();
}
}
std::hint::black_box(sum);
let divisor = (reps * passes) as u32;
let row = vec![
BenchResult {
name: "IO",
duration: times[0] / divisor,
},
BenchResult {
name: "Mmap",
duration: times[1] / divisor,
},
BenchResult {
name: "fold",
duration: times[2] / divisor,
},
BenchResult {
name: "try_fold",
duration: times[3] / divisor,
},
BenchResult {
name: "dyn",
duration: times[4] / divisor,
},
BenchResult {
name: "static",
duration: times[5] / divisor,
},
];
print_range_row(range_size, &row);
}
let ro = vec.read_only_clone();
let ro_passes = range_passes();
println!("\n--- Read-only full scan ({ro_passes} passes, random order) ---");
let ro_method_names: [&str; 4] = [
"ro_fold_range",
"ro_try_fold_range",
"ro_for_each_dyn",
"ro_for_each",
];
let mut ro_times = [Duration::ZERO; 4];
let mut sum = 0u64;
let mut rng = 0xBEEFu64;
for _pass in 0..ro_passes {
let mut order = [0usize, 1, 2, 3];
for i in (1..4).rev() {
let j = xorshift(&mut rng) as usize % (i + 1);
order.swap(i, j);
}
for &method in &order {
if can_purge {
drop_caches();
}
let t = Instant::now();
match method {
0 => sum = ro.fold_range(0, count, sum, |a, v: u64| a.wrapping_add(v)),
1 => {
sum = ro
.try_fold_range(0, count, sum, |a, v: u64| Ok::<_, ()>(a.wrapping_add(v)))
.unwrap()
}
2 => ro.for_each_range_dyn(0, count, &mut |v: u64| sum = sum.wrapping_add(v)),
3 => ro.for_each_range(0, count, |v: u64| sum = sum.wrapping_add(v)),
_ => unreachable!(),
}
ro_times[method] += t.elapsed();
}
}
std::hint::black_box(sum);
let ro_results: Vec<BenchResult> = (0..4)
.map(|i| BenchResult {
name: ro_method_names[i],
duration: ro_times[i] / ro_passes as u32,
})
.collect();
print_full_results(&ro_results, total_bytes);
let ro_passes = range_passes();
println!("\n--- Read-only range scans ({ro_passes} passes, random order) ---");
let ro_columns: Vec<&str> = vec!["fold", "try_fold", "dyn", "static"];
print_range_header(&ro_columns);
for &range_size in &ranges {
let reps = repetitions(range_size);
let max_start = count.saturating_sub(range_size);
let starts = random_starts(reps, max_start);
let mut times = [Duration::ZERO; 4];
let mut sum = 0u64;
let mut rng = range_size as u64 ^ 0xFACE;
for _pass in 0..ro_passes {
let mut order = [0usize, 1, 2, 3];
for i in (1..4).rev() {
let j = xorshift(&mut rng) as usize % (i + 1);
order.swap(i, j);
}
for &method in &order {
if can_purge {
drop_caches();
}
let t = Instant::now();
for &s in &starts {
let from = s;
let to = s + range_size;
match method {
0 => sum = ro.fold_range(from, to, sum, |a, v: u64| a.wrapping_add(v)),
1 => {
sum = ro
.try_fold_range(from, to, sum, |a, v: u64| {
Ok::<_, ()>(a.wrapping_add(v))
})
.unwrap()
}
2 => {
ro.for_each_range_dyn(from, to, &mut |v: u64| sum = sum.wrapping_add(v))
}
3 => ro.for_each_range(from, to, |v: u64| sum = sum.wrapping_add(v)),
_ => unreachable!(),
}
}
times[method] += t.elapsed();
}
}
std::hint::black_box(sum);
let divisor = (reps * ro_passes) as u32;
let row = vec![
BenchResult {
name: "fold",
duration: times[0] / divisor,
},
BenchResult {
name: "try_fold",
duration: times[1] / divisor,
},
BenchResult {
name: "dyn",
duration: times[2] / divisor,
},
BenchResult {
name: "static",
duration: times[3] / divisor,
},
];
print_range_row(range_size, &row);
}
}
fn bench_dir() -> std::path::PathBuf {
std::env::temp_dir().join("vecdb_bench")
}
fn cleanup_bench_dir() {
let dir = bench_dir();
if dir.exists() {
eprint!(" Cleaning up previous bench data...");
flush();
std::fs::remove_dir_all(&dir).ok();
eprintln!(" done");
}
}
fn run_bench<
V: ReadableVec<usize, u64> + StoredFold + AnyStoredVec + StoredVec<I = usize, T = u64>,
>(
label: &str,
count: usize,
can_purge: bool,
) {
cleanup_bench_dir();
let dir = bench_dir();
std::fs::create_dir_all(&dir).unwrap();
let db = Database::open(&dir).unwrap();
let vec = populate::<V>(&db, label, count);
bench_vec(&vec, label, count, can_purge);
drop(vec);
drop(db);
std::fs::remove_dir_all(&dir).ok();
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let mode = args.get(1).map(|s| s.as_str()).unwrap_or("all");
let count = value_count();
println!("BENCH_COUNT={count} (set env to override, e.g. BENCH_COUNT=1_000_000)");
let can_purge = check_cache_eviction();
println!();
let run_bytes = matches!(mode, "all" | "bytes" | "raw");
let run_zerocopy = matches!(mode, "all" | "zerocopy" | "raw");
let run_lz4 = matches!(mode, "all" | "lz4" | "compressed");
let run_pco = matches!(mode, "all" | "pco" | "compressed");
let run_zstd = matches!(mode, "all" | "zstd" | "compressed");
if run_bytes {
run_bench::<BytesVec<usize, u64>>("BytesVec<usize, u64>", count, can_purge);
}
if run_zerocopy {
run_bench::<ZeroCopyVec<usize, u64>>("ZeroCopyVec<usize, u64>", count, can_purge);
}
if run_lz4 {
run_bench::<LZ4Vec<usize, u64>>("LZ4Vec<usize, u64>", count, can_purge);
}
if run_pco {
run_bench::<PcoVec<usize, u64>>("PcoVec<usize, u64>", count, can_purge);
}
if run_zstd {
run_bench::<ZstdVec<usize, u64>>("ZstdVec<usize, u64>", count, can_purge);
}
cleanup_bench_dir();
}
fn flush() {
std::io::Write::flush(&mut std::io::stderr()).ok();
std::io::Write::flush(&mut std::io::stdout()).ok();
}