Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
204
crates/ruvector-core/benches/batch_operations.rs
Normal file
204
crates/ruvector-core/benches/batch_operations.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ruvector_core::types::{DistanceMetric, SearchQuery};
|
||||
use ruvector_core::{DbOptions, VectorDB, VectorEntry};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn bench_batch_insert(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("batch_insert");
|
||||
|
||||
for batch_size in [100, 1000, 10000].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(batch_size),
|
||||
batch_size,
|
||||
|bench, &size| {
|
||||
bench.iter_batched(
|
||||
|| {
|
||||
// Setup: Create DB and vectors
|
||||
let dir = tempdir().unwrap();
|
||||
let mut options = DbOptions::default();
|
||||
options.storage_path =
|
||||
dir.path().join("bench.db").to_string_lossy().to_string();
|
||||
options.dimensions = 128;
|
||||
options.hnsw_config = None; // Use flat index for faster insertion
|
||||
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
|
||||
let vectors: Vec<VectorEntry> = (0..size)
|
||||
.map(|i| VectorEntry {
|
||||
id: Some(format!("vec_{}", i)),
|
||||
vector: (0..128).map(|j| ((i + j) as f32) * 0.01).collect(),
|
||||
metadata: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
(db, vectors, dir)
|
||||
},
|
||||
|(db, vectors, _dir)| {
|
||||
// Benchmark: Batch insert
|
||||
db.insert_batch(black_box(vectors)).unwrap()
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_individual_insert_vs_batch(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("individual_vs_batch_insert");
|
||||
let size = 1000;
|
||||
|
||||
// Individual inserts
|
||||
group.bench_function("individual_1000", |bench| {
|
||||
bench.iter_batched(
|
||||
|| {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut options = DbOptions::default();
|
||||
options.storage_path = dir.path().join("bench.db").to_string_lossy().to_string();
|
||||
options.dimensions = 64;
|
||||
options.hnsw_config = None;
|
||||
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
let vectors: Vec<VectorEntry> = (0..size)
|
||||
.map(|i| VectorEntry {
|
||||
id: Some(format!("vec_{}", i)),
|
||||
vector: vec![i as f32; 64],
|
||||
metadata: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
(db, vectors, dir)
|
||||
},
|
||||
|(db, vectors, _dir)| {
|
||||
for vector in vectors {
|
||||
db.insert(black_box(vector)).unwrap();
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Batch insert
|
||||
group.bench_function("batch_1000", |bench| {
|
||||
bench.iter_batched(
|
||||
|| {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut options = DbOptions::default();
|
||||
options.storage_path = dir.path().join("bench.db").to_string_lossy().to_string();
|
||||
options.dimensions = 64;
|
||||
options.hnsw_config = None;
|
||||
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
let vectors: Vec<VectorEntry> = (0..size)
|
||||
.map(|i| VectorEntry {
|
||||
id: Some(format!("vec_{}", i)),
|
||||
vector: vec![i as f32; 64],
|
||||
metadata: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
(db, vectors, dir)
|
||||
},
|
||||
|(db, vectors, _dir)| db.insert_batch(black_box(vectors)).unwrap(),
|
||||
criterion::BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_parallel_searches(c: &mut Criterion) {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut options = DbOptions::default();
|
||||
options.storage_path = dir
|
||||
.path()
|
||||
.join("search_bench.db")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
options.dimensions = 128;
|
||||
options.distance_metric = DistanceMetric::Cosine;
|
||||
options.hnsw_config = None;
|
||||
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
|
||||
// Insert test data
|
||||
let vectors: Vec<VectorEntry> = (0..1000)
|
||||
.map(|i| VectorEntry {
|
||||
id: Some(format!("vec_{}", i)),
|
||||
vector: (0..128).map(|j| ((i + j) as f32) * 0.01).collect(),
|
||||
metadata: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
db.insert_batch(vectors).unwrap();
|
||||
|
||||
// Benchmark multiple sequential searches
|
||||
c.bench_function("sequential_searches_100", |bench| {
|
||||
bench.iter(|| {
|
||||
for i in 0..100 {
|
||||
let query: Vec<f32> = (0..128).map(|j| ((i + j) as f32) * 0.01).collect();
|
||||
let _ = db
|
||||
.search(SearchQuery {
|
||||
vector: black_box(query),
|
||||
k: 10,
|
||||
filter: None,
|
||||
ef_search: None,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_batch_delete(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("batch_delete");
|
||||
|
||||
for size in [100, 1000].iter() {
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |bench, &size| {
|
||||
bench.iter_batched(
|
||||
|| {
|
||||
// Setup: Create DB with vectors
|
||||
let dir = tempdir().unwrap();
|
||||
let mut options = DbOptions::default();
|
||||
options.storage_path =
|
||||
dir.path().join("bench.db").to_string_lossy().to_string();
|
||||
options.dimensions = 32;
|
||||
options.hnsw_config = None;
|
||||
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
|
||||
let vectors: Vec<VectorEntry> = (0..size)
|
||||
.map(|i| VectorEntry {
|
||||
id: Some(format!("vec_{}", i)),
|
||||
vector: vec![i as f32; 32],
|
||||
metadata: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let ids = db.insert_batch(vectors).unwrap();
|
||||
(db, ids, dir)
|
||||
},
|
||||
|(db, ids, _dir)| {
|
||||
// Benchmark: Delete all
|
||||
for id in ids {
|
||||
db.delete(black_box(&id)).unwrap();
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_batch_insert,
|
||||
bench_individual_insert_vs_batch,
|
||||
bench_parallel_searches,
|
||||
bench_batch_delete
|
||||
);
|
||||
criterion_main!(benches);
|
||||
474
crates/ruvector-core/benches/bench_memory.rs
Normal file
474
crates/ruvector-core/benches/bench_memory.rs
Normal file
@@ -0,0 +1,474 @@
|
||||
//! Memory Allocation and Pool Benchmarks
|
||||
//!
|
||||
//! This module benchmarks arena allocation, cache-optimized storage,
|
||||
//! and memory access patterns.
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ruvector_core::arena::Arena;
|
||||
use ruvector_core::cache_optimized::SoAVectorStorage;
|
||||
|
||||
// ============================================================================
|
||||
// Arena Allocation Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_arena_allocation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("arena_allocation");
|
||||
|
||||
for count in [10, 100, 1000, 10000] {
|
||||
group.throughput(Throughput::Elements(count));
|
||||
|
||||
// Benchmark arena allocation
|
||||
group.bench_with_input(BenchmarkId::new("arena", count), &count, |bench, &count| {
|
||||
bench.iter(|| {
|
||||
let arena = Arena::new(1024 * 1024);
|
||||
for _ in 0..count {
|
||||
let _vec = arena.alloc_vec::<f32>(black_box(64));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Compare with standard Vec allocation
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("std_vec", count),
|
||||
&count,
|
||||
|bench, &count| {
|
||||
bench.iter(|| {
|
||||
let mut vecs = Vec::with_capacity(count as usize);
|
||||
for _ in 0..count {
|
||||
vecs.push(Vec::<f32>::with_capacity(black_box(64)));
|
||||
}
|
||||
vecs
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_arena_allocation_sizes(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("arena_allocation_sizes");
|
||||
|
||||
for size in [8, 32, 64, 128, 256, 512, 1024, 4096] {
|
||||
group.throughput(Throughput::Bytes(size as u64 * 4)); // f32 = 4 bytes
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("alloc", size), &size, |bench, &size| {
|
||||
bench.iter(|| {
|
||||
let arena = Arena::new(1024 * 1024);
|
||||
for _ in 0..1000 {
|
||||
let _vec = arena.alloc_vec::<f32>(black_box(size));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_arena_reset_reuse(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("arena_reset_reuse");
|
||||
|
||||
for iterations in [10, 100, 1000] {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("with_reset", iterations),
|
||||
&iterations,
|
||||
|bench, &iterations| {
|
||||
bench.iter(|| {
|
||||
let arena = Arena::new(1024 * 1024);
|
||||
for _ in 0..iterations {
|
||||
// Allocate
|
||||
for _ in 0..100 {
|
||||
let _vec = arena.alloc_vec::<f32>(64);
|
||||
}
|
||||
// Reset for reuse
|
||||
arena.reset();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("without_reset", iterations),
|
||||
&iterations,
|
||||
|bench, &iterations| {
|
||||
bench.iter(|| {
|
||||
for _ in 0..iterations {
|
||||
let arena = Arena::new(1024 * 1024);
|
||||
for _ in 0..100 {
|
||||
let _vec = arena.alloc_vec::<f32>(64);
|
||||
}
|
||||
// No reset, create new arena each time
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_arena_push_operations(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("arena_push");
|
||||
|
||||
for count in [100, 1000, 10000] {
|
||||
group.throughput(Throughput::Elements(count));
|
||||
|
||||
// Arena push
|
||||
group.bench_with_input(BenchmarkId::new("arena", count), &count, |bench, &count| {
|
||||
bench.iter(|| {
|
||||
let arena = Arena::new(1024 * 1024);
|
||||
let mut vec = arena.alloc_vec::<f32>(count as usize);
|
||||
for i in 0..count {
|
||||
vec.push(black_box(i as f32));
|
||||
}
|
||||
vec
|
||||
});
|
||||
});
|
||||
|
||||
// Standard Vec push
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("std_vec", count),
|
||||
&count,
|
||||
|bench, &count| {
|
||||
bench.iter(|| {
|
||||
let mut vec = Vec::with_capacity(count as usize);
|
||||
for i in 0..count {
|
||||
vec.push(black_box(i as f32));
|
||||
}
|
||||
vec
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SoA Vector Storage Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_soa_storage_push(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("soa_storage_push");
|
||||
|
||||
for dim in [64, 128, 256, 384, 512, 768] {
|
||||
let vector: Vec<f32> = (0..dim).map(|i| i as f32 * 0.01).collect();
|
||||
|
||||
group.throughput(Throughput::Elements(dim as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("soa", dim), &dim, |bench, _| {
|
||||
bench.iter(|| {
|
||||
let mut storage = SoAVectorStorage::new(dim, 128);
|
||||
for _ in 0..1000 {
|
||||
storage.push(black_box(&vector));
|
||||
}
|
||||
storage
|
||||
});
|
||||
});
|
||||
|
||||
// Compare with Vec<Vec<f32>>
|
||||
group.bench_with_input(BenchmarkId::new("vec_of_vec", dim), &dim, |bench, _| {
|
||||
bench.iter(|| {
|
||||
let mut storage: Vec<Vec<f32>> = Vec::with_capacity(1000);
|
||||
for _ in 0..1000 {
|
||||
storage.push(black_box(vector.clone()));
|
||||
}
|
||||
storage
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_soa_storage_get(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("soa_storage_get");
|
||||
|
||||
for dim in [128, 384, 768] {
|
||||
let mut storage = SoAVectorStorage::new(dim, 128);
|
||||
|
||||
for i in 0..10000 {
|
||||
let vector: Vec<f32> = (0..dim).map(|j| (i * dim + j) as f32 * 0.001).collect();
|
||||
storage.push(&vector);
|
||||
}
|
||||
|
||||
let mut output = vec![0.0_f32; dim];
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sequential", dim), &dim, |bench, _| {
|
||||
bench.iter(|| {
|
||||
for i in 0..10000 {
|
||||
storage.get(black_box(i), &mut output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("random", dim), &dim, |bench, _| {
|
||||
let indices: Vec<usize> = (0..10000).map(|i| (i * 37 + 13) % 10000).collect();
|
||||
bench.iter(|| {
|
||||
for &idx in &indices {
|
||||
storage.get(black_box(idx), &mut output);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_soa_dimension_slice(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("soa_dimension_slice");
|
||||
|
||||
for dim in [64, 128, 256, 512] {
|
||||
let mut storage = SoAVectorStorage::new(dim, 128);
|
||||
|
||||
for i in 0..10000 {
|
||||
let vector: Vec<f32> = (0..dim).map(|j| (i * dim + j) as f32 * 0.001).collect();
|
||||
storage.push(&vector);
|
||||
}
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("access_all_dims", dim),
|
||||
&dim,
|
||||
|bench, &dim| {
|
||||
bench.iter(|| {
|
||||
let mut sum = 0.0_f32;
|
||||
for d in 0..dim {
|
||||
let slice = storage.dimension_slice(black_box(d));
|
||||
sum += slice.iter().sum::<f32>();
|
||||
}
|
||||
sum
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("access_single_dim", dim),
|
||||
&dim,
|
||||
|bench, _| {
|
||||
bench.iter(|| {
|
||||
let slice = storage.dimension_slice(black_box(0));
|
||||
slice.iter().sum::<f32>()
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_soa_batch_distances(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("soa_batch_distances");
|
||||
|
||||
for (dim, count) in [
|
||||
(128, 1000),
|
||||
(384, 1000),
|
||||
(768, 1000),
|
||||
(128, 10000),
|
||||
(384, 5000),
|
||||
] {
|
||||
let mut storage = SoAVectorStorage::new(dim, 128);
|
||||
|
||||
for i in 0..count {
|
||||
let vector: Vec<f32> = (0..dim)
|
||||
.map(|j| ((i * dim + j) % 1000) as f32 * 0.001)
|
||||
.collect();
|
||||
storage.push(&vector);
|
||||
}
|
||||
|
||||
let query: Vec<f32> = (0..dim).map(|j| j as f32 * 0.002).collect();
|
||||
let mut distances = vec![0.0_f32; count];
|
||||
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(format!("{}d_x{}", dim, count), dim),
|
||||
&dim,
|
||||
|bench, _| {
|
||||
bench.iter(|| {
|
||||
storage.batch_euclidean_distances(black_box(&query), &mut distances);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Memory Access Pattern Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_memory_layout_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("memory_layout");
|
||||
|
||||
let dim = 384;
|
||||
let count = 10000;
|
||||
|
||||
// SoA layout
|
||||
let mut soa_storage = SoAVectorStorage::new(dim, 128);
|
||||
for i in 0..count {
|
||||
let vector: Vec<f32> = (0..dim)
|
||||
.map(|j| ((i * dim + j) % 1000) as f32 * 0.001)
|
||||
.collect();
|
||||
soa_storage.push(&vector);
|
||||
}
|
||||
|
||||
// AoS layout (Vec<Vec<f32>>)
|
||||
let aos_storage: Vec<Vec<f32>> = (0..count)
|
||||
.map(|i| {
|
||||
(0..dim)
|
||||
.map(|j| ((i * dim + j) % 1000) as f32 * 0.001)
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let query: Vec<f32> = (0..dim).map(|j| j as f32 * 0.002).collect();
|
||||
let mut soa_distances = vec![0.0_f32; count];
|
||||
|
||||
group.bench_function("soa_batch_euclidean", |bench| {
|
||||
bench.iter(|| {
|
||||
soa_storage.batch_euclidean_distances(black_box(&query), &mut soa_distances);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("aos_naive_euclidean", |bench| {
|
||||
bench.iter(|| {
|
||||
let distances: Vec<f32> = aos_storage
|
||||
.iter()
|
||||
.map(|v| {
|
||||
query
|
||||
.iter()
|
||||
.zip(v.iter())
|
||||
.map(|(a, b)| (a - b) * (a - b))
|
||||
.sum::<f32>()
|
||||
.sqrt()
|
||||
})
|
||||
.collect();
|
||||
distances
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_cache_efficiency(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cache_efficiency");
|
||||
|
||||
let dim = 512;
|
||||
|
||||
// Test with different vector counts to observe cache effects
|
||||
for count in [100, 1000, 10000, 50000] {
|
||||
let mut storage = SoAVectorStorage::new(dim, 128);
|
||||
|
||||
for i in 0..count {
|
||||
let vector: Vec<f32> = (0..dim)
|
||||
.map(|j| ((i * dim + j) % 1000) as f32 * 0.001)
|
||||
.collect();
|
||||
storage.push(&vector);
|
||||
}
|
||||
|
||||
let query: Vec<f32> = (0..dim).map(|j| j as f32 * 0.001).collect();
|
||||
let mut distances = vec![0.0_f32; count];
|
||||
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("batch_distance", count),
|
||||
&count,
|
||||
|bench, _| {
|
||||
bench.iter(|| {
|
||||
storage.batch_euclidean_distances(black_box(&query), &mut distances);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Growth and Reallocation Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_soa_growth(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("soa_growth");
|
||||
|
||||
// Test growth from small initial capacity
|
||||
group.bench_function("grow_from_small", |bench| {
|
||||
bench.iter(|| {
|
||||
let mut storage = SoAVectorStorage::new(128, 4); // Very small initial
|
||||
for i in 0..10000 {
|
||||
let vector: Vec<f32> = (0..128).map(|j| (i * 128 + j) as f32 * 0.001).collect();
|
||||
storage.push(black_box(&vector));
|
||||
}
|
||||
storage
|
||||
});
|
||||
});
|
||||
|
||||
// Test with pre-allocated capacity
|
||||
group.bench_function("preallocated", |bench| {
|
||||
bench.iter(|| {
|
||||
let mut storage = SoAVectorStorage::new(128, 16384); // Pre-allocate
|
||||
for i in 0..10000 {
|
||||
let vector: Vec<f32> = (0..128).map(|j| (i * 128 + j) as f32 * 0.001).collect();
|
||||
storage.push(black_box(&vector));
|
||||
}
|
||||
storage
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mixed Type Allocation Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_arena_mixed_types(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("arena_mixed_types");
|
||||
|
||||
group.bench_function("mixed_allocations", |bench| {
|
||||
bench.iter(|| {
|
||||
let arena = Arena::new(1024 * 1024);
|
||||
for _ in 0..100 {
|
||||
let _f32_vec = arena.alloc_vec::<f32>(black_box(64));
|
||||
let _f64_vec = arena.alloc_vec::<f64>(black_box(32));
|
||||
let _u32_vec = arena.alloc_vec::<u32>(black_box(128));
|
||||
let _u8_vec = arena.alloc_vec::<u8>(black_box(256));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("uniform_allocations", |bench| {
|
||||
bench.iter(|| {
|
||||
let arena = Arena::new(1024 * 1024);
|
||||
for _ in 0..400 {
|
||||
let _f32_vec = arena.alloc_vec::<f32>(black_box(64));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Criterion Groups
|
||||
// ============================================================================
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_arena_allocation,
|
||||
bench_arena_allocation_sizes,
|
||||
bench_arena_reset_reuse,
|
||||
bench_arena_push_operations,
|
||||
bench_soa_storage_push,
|
||||
bench_soa_storage_get,
|
||||
bench_soa_dimension_slice,
|
||||
bench_soa_batch_distances,
|
||||
bench_memory_layout_comparison,
|
||||
bench_cache_efficiency,
|
||||
bench_soa_growth,
|
||||
bench_arena_mixed_types,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
335
crates/ruvector-core/benches/bench_simd.rs
Normal file
335
crates/ruvector-core/benches/bench_simd.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
//! SIMD Performance Benchmarks
|
||||
//!
|
||||
//! This module benchmarks SIMD-optimized distance calculations
|
||||
//! across various vector dimensions and operation types.
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ruvector_core::simd_intrinsics::*;
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
fn generate_vectors(dim: usize) -> (Vec<f32>, Vec<f32>) {
|
||||
let a: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.01).collect();
|
||||
let b: Vec<f32> = (0..dim).map(|i| ((i + 100) as f32) * 0.01).collect();
|
||||
(a, b)
|
||||
}
|
||||
|
||||
fn generate_batch_vectors(dim: usize, count: usize) -> (Vec<f32>, Vec<Vec<f32>>) {
|
||||
let query: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.01).collect();
|
||||
let vectors: Vec<Vec<f32>> = (0..count)
|
||||
.map(|j| (0..dim).map(|i| ((i + j * 10) as f32) * 0.01).collect())
|
||||
.collect();
|
||||
(query, vectors)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Euclidean Distance Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_euclidean_by_dimension(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("euclidean_by_dimension");
|
||||
|
||||
for dim in [32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048] {
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
group.throughput(Throughput::Elements(dim as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("simd", dim), &dim, |bench, _| {
|
||||
bench.iter(|| euclidean_distance_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_euclidean_small_vectors(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("euclidean_small_vectors");
|
||||
|
||||
// Test small vector sizes that may not benefit from SIMD
|
||||
for dim in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 16] {
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("simd", dim), &dim, |bench, _| {
|
||||
bench.iter(|| euclidean_distance_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_euclidean_non_aligned(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("euclidean_non_aligned");
|
||||
|
||||
// Test non-SIMD-aligned sizes
|
||||
for dim in [31, 33, 63, 65, 127, 129, 255, 257, 383, 385, 511, 513] {
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("simd", dim), &dim, |bench, _| {
|
||||
bench.iter(|| euclidean_distance_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dot Product Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_dot_product_by_dimension(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("dot_product_by_dimension");
|
||||
|
||||
for dim in [32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048] {
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
group.throughput(Throughput::Elements(dim as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("simd", dim), &dim, |bench, _| {
|
||||
bench.iter(|| dot_product_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_dot_product_common_embeddings(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("dot_product_common_embeddings");
|
||||
|
||||
// Common embedding model dimensions
|
||||
let dims = [
|
||||
(128, "small"),
|
||||
(384, "all-MiniLM-L6"),
|
||||
(512, "e5-small"),
|
||||
(768, "all-mpnet-base"),
|
||||
(1024, "e5-large"),
|
||||
(1536, "text-embedding-ada-002"),
|
||||
(2048, "llama-7b-hidden"),
|
||||
];
|
||||
|
||||
for (dim, name) in dims {
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new(name, dim), &dim, |bench, _| {
|
||||
bench.iter(|| dot_product_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cosine Similarity Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_cosine_by_dimension(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cosine_by_dimension");
|
||||
|
||||
for dim in [32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048] {
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
group.throughput(Throughput::Elements(dim as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("simd", dim), &dim, |bench, _| {
|
||||
bench.iter(|| cosine_similarity_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Manhattan Distance Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_manhattan_by_dimension(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("manhattan_by_dimension");
|
||||
|
||||
for dim in [32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048] {
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
group.throughput(Throughput::Elements(dim as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("simd", dim), &dim, |bench, _| {
|
||||
bench.iter(|| manhattan_distance_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Batch Operations Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_batch_euclidean(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("batch_euclidean");
|
||||
|
||||
for count in [10, 100, 1000, 10000] {
|
||||
let (query, vectors) = generate_batch_vectors(384, count);
|
||||
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("384d", count), &count, |bench, _| {
|
||||
bench.iter(|| {
|
||||
for v in &vectors {
|
||||
euclidean_distance_simd(black_box(&query), black_box(v));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_batch_dot_product(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("batch_dot_product");
|
||||
|
||||
for count in [10, 100, 1000, 10000] {
|
||||
let (query, vectors) = generate_batch_vectors(768, count);
|
||||
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("768d", count), &count, |bench, _| {
|
||||
bench.iter(|| {
|
||||
for v in &vectors {
|
||||
dot_product_simd(black_box(&query), black_box(v));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Comparison Benchmarks (All Metrics)
|
||||
// ============================================================================
|
||||
|
||||
fn bench_all_metrics_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("metrics_comparison");
|
||||
|
||||
let dim = 384; // Common embedding dimension
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
group.bench_function("euclidean", |bench| {
|
||||
bench.iter(|| euclidean_distance_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
|
||||
group.bench_function("dot_product", |bench| {
|
||||
bench.iter(|| dot_product_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
|
||||
group.bench_function("cosine", |bench| {
|
||||
bench.iter(|| cosine_similarity_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
|
||||
group.bench_function("manhattan", |bench| {
|
||||
bench.iter(|| manhattan_distance_simd(black_box(&a), black_box(&b)));
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Memory Access Pattern Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_sequential_vs_random_access(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("access_patterns");
|
||||
|
||||
let dim = 512;
|
||||
let count = 1000;
|
||||
|
||||
// Generate vectors
|
||||
let vectors: Vec<Vec<f32>> = (0..count)
|
||||
.map(|j| (0..dim).map(|i| ((i + j * 10) as f32) * 0.01).collect())
|
||||
.collect();
|
||||
let query: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.01).collect();
|
||||
|
||||
// Sequential access indices
|
||||
let sequential_indices: Vec<usize> = (0..count).collect();
|
||||
|
||||
// Random-ish access indices
|
||||
let random_indices: Vec<usize> = (0..count)
|
||||
.map(|i| (i * 37 + 13) % count) // Pseudo-random
|
||||
.collect();
|
||||
|
||||
group.bench_function("sequential", |bench| {
|
||||
bench.iter(|| {
|
||||
for &idx in &sequential_indices {
|
||||
euclidean_distance_simd(black_box(&query), black_box(&vectors[idx]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("random", |bench| {
|
||||
bench.iter(|| {
|
||||
for &idx in &random_indices {
|
||||
euclidean_distance_simd(black_box(&query), black_box(&vectors[idx]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Throughput Measurement
|
||||
// ============================================================================
|
||||
|
||||
fn bench_throughput_ops_per_second(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("throughput");
|
||||
group.sample_size(50);
|
||||
|
||||
for dim in [128, 384, 768, 1536] {
|
||||
let (a, b) = generate_vectors(dim);
|
||||
|
||||
// Report throughput in operations/second
|
||||
group.bench_with_input(BenchmarkId::new("euclidean_ops", dim), &dim, |bench, _| {
|
||||
bench.iter(|| {
|
||||
// Perform 100 operations per iteration
|
||||
for _ in 0..100 {
|
||||
euclidean_distance_simd(black_box(&a), black_box(&b));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("dot_product_ops", dim),
|
||||
&dim,
|
||||
|bench, _| {
|
||||
bench.iter(|| {
|
||||
for _ in 0..100 {
|
||||
dot_product_simd(black_box(&a), black_box(&b));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Criterion Groups
|
||||
// ============================================================================
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_euclidean_by_dimension,
|
||||
bench_euclidean_small_vectors,
|
||||
bench_euclidean_non_aligned,
|
||||
bench_dot_product_by_dimension,
|
||||
bench_dot_product_common_embeddings,
|
||||
bench_cosine_by_dimension,
|
||||
bench_manhattan_by_dimension,
|
||||
bench_batch_euclidean,
|
||||
bench_batch_dot_product,
|
||||
bench_all_metrics_comparison,
|
||||
bench_sequential_vs_random_access,
|
||||
bench_throughput_ops_per_second,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
262
crates/ruvector-core/benches/comprehensive_bench.rs
Normal file
262
crates/ruvector-core/benches/comprehensive_bench.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ruvector_core::arena::Arena;
|
||||
use ruvector_core::cache_optimized::SoAVectorStorage;
|
||||
use ruvector_core::distance::*;
|
||||
use ruvector_core::lockfree::{LockFreeCounter, LockFreeStats, ObjectPool};
|
||||
use ruvector_core::simd_intrinsics::*;
|
||||
use ruvector_core::types::DistanceMetric;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
// Benchmark SIMD intrinsics vs SimSIMD
|
||||
fn bench_simd_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("simd_comparison");
|
||||
|
||||
for size in [128, 384, 768, 1536].iter() {
|
||||
let a: Vec<f32> = (0..*size).map(|i| i as f32 * 0.1).collect();
|
||||
let b: Vec<f32> = (0..*size).map(|i| (i + 1) as f32 * 0.1).collect();
|
||||
|
||||
// Euclidean distance
|
||||
group.throughput(Throughput::Elements(*size as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("euclidean_simsimd", size),
|
||||
size,
|
||||
|bench, _| {
|
||||
bench.iter(|| euclidean_distance(black_box(&a), black_box(&b)));
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("euclidean_avx2", size),
|
||||
size,
|
||||
|bench, _| {
|
||||
bench.iter(|| euclidean_distance_avx2(black_box(&a), black_box(&b)));
|
||||
},
|
||||
);
|
||||
|
||||
// Dot product
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("dot_product_simsimd", size),
|
||||
size,
|
||||
|bench, _| {
|
||||
bench.iter(|| dot_product_distance(black_box(&a), black_box(&b)));
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("dot_product_avx2", size),
|
||||
size,
|
||||
|bench, _| {
|
||||
bench.iter(|| dot_product_avx2(black_box(&a), black_box(&b)));
|
||||
},
|
||||
);
|
||||
|
||||
// Cosine similarity
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("cosine_simsimd", size),
|
||||
size,
|
||||
|bench, _| {
|
||||
bench.iter(|| cosine_distance(black_box(&a), black_box(&b)));
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("cosine_avx2", size), size, |bench, _| {
|
||||
bench.iter(|| cosine_similarity_avx2(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// Benchmark Structure-of-Arrays vs Array-of-Structures
|
||||
fn bench_cache_optimization(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cache_optimization");
|
||||
|
||||
let dimensions = 384;
|
||||
let num_vectors = 10000;
|
||||
|
||||
// Prepare data
|
||||
let vectors: Vec<Vec<f32>> = (0..num_vectors)
|
||||
.map(|i| (0..dimensions).map(|j| (i * j) as f32 * 0.001).collect())
|
||||
.collect();
|
||||
|
||||
let query: Vec<f32> = (0..dimensions).map(|i| i as f32 * 0.01).collect();
|
||||
|
||||
// Array-of-Structures (traditional Vec<Vec<f32>>)
|
||||
group.bench_function("aos_batch_distance", |bench| {
|
||||
bench.iter(|| {
|
||||
let mut distances: Vec<f32> = Vec::with_capacity(num_vectors);
|
||||
for vector in &vectors {
|
||||
let dist = euclidean_distance(black_box(&query), black_box(vector));
|
||||
distances.push(dist);
|
||||
}
|
||||
black_box(distances)
|
||||
});
|
||||
});
|
||||
|
||||
// Structure-of-Arrays
|
||||
let mut soa_storage = SoAVectorStorage::new(dimensions, num_vectors);
|
||||
for vector in &vectors {
|
||||
soa_storage.push(vector);
|
||||
}
|
||||
|
||||
group.bench_function("soa_batch_distance", |bench| {
|
||||
bench.iter(|| {
|
||||
let mut distances = vec![0.0; num_vectors];
|
||||
soa_storage.batch_euclidean_distances(black_box(&query), &mut distances);
|
||||
black_box(distances)
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// Benchmark arena allocation vs standard allocation
|
||||
fn bench_arena_allocation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("arena_allocation");
|
||||
|
||||
let num_allocations = 1000;
|
||||
let vec_size = 100;
|
||||
|
||||
group.bench_function("standard_allocation", |bench| {
|
||||
bench.iter(|| {
|
||||
let mut vecs = Vec::new();
|
||||
for _ in 0..num_allocations {
|
||||
let mut v = Vec::with_capacity(vec_size);
|
||||
for j in 0..vec_size {
|
||||
v.push(j as f32);
|
||||
}
|
||||
vecs.push(v);
|
||||
}
|
||||
black_box(vecs)
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("arena_allocation", |bench| {
|
||||
bench.iter(|| {
|
||||
let arena = Arena::new(1024 * 1024);
|
||||
let mut vecs = Vec::new();
|
||||
for _ in 0..num_allocations {
|
||||
let mut v = arena.alloc_vec::<f32>(vec_size);
|
||||
for j in 0..vec_size {
|
||||
v.push(j as f32);
|
||||
}
|
||||
vecs.push(v);
|
||||
}
|
||||
black_box(vecs)
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// Benchmark lock-free operations vs locked operations
|
||||
fn bench_lockfree(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("lockfree");
|
||||
|
||||
// Counter benchmark
|
||||
group.bench_function("lockfree_counter_single_thread", |bench| {
|
||||
let counter = LockFreeCounter::new(0);
|
||||
bench.iter(|| {
|
||||
for _ in 0..10000 {
|
||||
counter.increment();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("lockfree_counter_multi_thread", |bench| {
|
||||
bench.iter(|| {
|
||||
let counter = Arc::new(LockFreeCounter::new(0));
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..4 {
|
||||
let counter_clone = Arc::clone(&counter);
|
||||
handles.push(thread::spawn(move || {
|
||||
for _ in 0..2500 {
|
||||
counter_clone.increment();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
black_box(counter.get())
|
||||
});
|
||||
});
|
||||
|
||||
// Stats collector benchmark
|
||||
group.bench_function("lockfree_stats", |bench| {
|
||||
let stats = LockFreeStats::new();
|
||||
bench.iter(|| {
|
||||
for i in 0..1000 {
|
||||
stats.record_query(i);
|
||||
}
|
||||
black_box(stats.snapshot())
|
||||
});
|
||||
});
|
||||
|
||||
// Object pool benchmark
|
||||
group.bench_function("object_pool_acquire_release", |bench| {
|
||||
let pool = ObjectPool::new(10, || Vec::<f32>::with_capacity(1000));
|
||||
bench.iter(|| {
|
||||
let mut obj = pool.acquire();
|
||||
for i in 0..100 {
|
||||
obj.push(i as f32);
|
||||
}
|
||||
black_box(&*obj);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// Benchmark thread scaling
|
||||
fn bench_thread_scaling(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("thread_scaling");
|
||||
|
||||
let dimensions = 384;
|
||||
let num_vectors = 10000;
|
||||
let query: Vec<f32> = (0..dimensions).map(|i| i as f32 * 0.01).collect();
|
||||
let vectors: Vec<Vec<f32>> = (0..num_vectors)
|
||||
.map(|i| (0..dimensions).map(|j| (i * j) as f32 * 0.001).collect())
|
||||
.collect();
|
||||
|
||||
for num_threads in [1, 2, 4, 8].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("parallel_distance", num_threads),
|
||||
num_threads,
|
||||
|bench, &threads| {
|
||||
bench.iter(|| {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(threads)
|
||||
.build()
|
||||
.unwrap()
|
||||
.install(|| {
|
||||
let result = batch_distances(
|
||||
black_box(&query),
|
||||
black_box(&vectors),
|
||||
DistanceMetric::Euclidean,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(result)
|
||||
})
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_simd_comparison,
|
||||
bench_cache_optimization,
|
||||
bench_arena_allocation,
|
||||
bench_lockfree,
|
||||
bench_thread_scaling
|
||||
);
|
||||
criterion_main!(benches);
|
||||
74
crates/ruvector-core/benches/distance_metrics.rs
Normal file
74
crates/ruvector-core/benches/distance_metrics.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ruvector_core::distance::*;
|
||||
use ruvector_core::types::DistanceMetric;
|
||||
|
||||
fn bench_euclidean(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("euclidean_distance");
|
||||
|
||||
for size in [128, 384, 768, 1536].iter() {
|
||||
let a: Vec<f32> = (0..*size).map(|i| i as f32).collect();
|
||||
let b: Vec<f32> = (0..*size).map(|i| (i + 1) as f32).collect();
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |bench, _| {
|
||||
bench.iter(|| euclidean_distance(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_cosine(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cosine_distance");
|
||||
|
||||
for size in [128, 384, 768, 1536].iter() {
|
||||
let a: Vec<f32> = (0..*size).map(|i| i as f32).collect();
|
||||
let b: Vec<f32> = (0..*size).map(|i| (i + 1) as f32).collect();
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |bench, _| {
|
||||
bench.iter(|| cosine_distance(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_dot_product(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("dot_product_distance");
|
||||
|
||||
for size in [128, 384, 768, 1536].iter() {
|
||||
let a: Vec<f32> = (0..*size).map(|i| i as f32).collect();
|
||||
let b: Vec<f32> = (0..*size).map(|i| (i + 1) as f32).collect();
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |bench, _| {
|
||||
bench.iter(|| dot_product_distance(black_box(&a), black_box(&b)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_batch_distances(c: &mut Criterion) {
|
||||
let query: Vec<f32> = (0..384).map(|i| i as f32).collect();
|
||||
let vectors: Vec<Vec<f32>> = (0..1000)
|
||||
.map(|_| (0..384).map(|i| (i as f32) * 1.1).collect())
|
||||
.collect();
|
||||
|
||||
c.bench_function("batch_distances_1000x384", |b| {
|
||||
b.iter(|| {
|
||||
batch_distances(
|
||||
black_box(&query),
|
||||
black_box(&vectors),
|
||||
DistanceMetric::Cosine,
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_euclidean,
|
||||
bench_cosine,
|
||||
bench_dot_product,
|
||||
bench_batch_distances
|
||||
);
|
||||
criterion_main!(benches);
|
||||
56
crates/ruvector-core/benches/hnsw_search.rs
Normal file
56
crates/ruvector-core/benches/hnsw_search.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ruvector_core::types::{DbOptions, DistanceMetric, HnswConfig, SearchQuery};
|
||||
use ruvector_core::{VectorDB, VectorEntry};
|
||||
|
||||
fn bench_hnsw_search(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("hnsw_search");
|
||||
|
||||
// Create temp database
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let options = DbOptions {
|
||||
dimensions: 128,
|
||||
distance_metric: DistanceMetric::Cosine,
|
||||
storage_path: temp_dir
|
||||
.path()
|
||||
.join("test.db")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
hnsw_config: Some(HnswConfig::default()),
|
||||
quantization: None,
|
||||
};
|
||||
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
|
||||
// Insert test vectors
|
||||
let vectors: Vec<VectorEntry> = (0..1000)
|
||||
.map(|i| VectorEntry {
|
||||
id: Some(format!("v{}", i)),
|
||||
vector: (0..128).map(|j| ((i + j) as f32) * 0.1).collect(),
|
||||
metadata: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
db.insert_batch(vectors).unwrap();
|
||||
|
||||
// Benchmark search
|
||||
let query: Vec<f32> = (0..128).map(|i| i as f32).collect();
|
||||
|
||||
for k in [1, 10, 100].iter() {
|
||||
group.bench_with_input(BenchmarkId::from_parameter(k), k, |bench, &k| {
|
||||
bench.iter(|| {
|
||||
db.search(SearchQuery {
|
||||
vector: black_box(query.clone()),
|
||||
k: black_box(k),
|
||||
filter: None,
|
||||
ef_search: None,
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_hnsw_search);
|
||||
criterion_main!(benches);
|
||||
77
crates/ruvector-core/benches/quantization_bench.rs
Normal file
77
crates/ruvector-core/benches/quantization_bench.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ruvector_core::quantization::*;
|
||||
|
||||
fn bench_scalar_quantization(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("scalar_quantization");
|
||||
|
||||
for size in [128, 384, 768, 1536].iter() {
|
||||
let vector: Vec<f32> = (0..*size).map(|i| i as f32 * 0.1).collect();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("encode", size), size, |bench, _| {
|
||||
bench.iter(|| ScalarQuantized::quantize(black_box(&vector)));
|
||||
});
|
||||
|
||||
let quantized = ScalarQuantized::quantize(&vector);
|
||||
group.bench_with_input(BenchmarkId::new("decode", size), size, |bench, _| {
|
||||
bench.iter(|| quantized.reconstruct());
|
||||
});
|
||||
|
||||
let quantized2 = ScalarQuantized::quantize(&vector);
|
||||
group.bench_with_input(BenchmarkId::new("distance", size), size, |bench, _| {
|
||||
bench.iter(|| quantized.distance(black_box(&quantized2)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_binary_quantization(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("binary_quantization");
|
||||
|
||||
for size in [128, 384, 768, 1536].iter() {
|
||||
let vector: Vec<f32> = (0..*size)
|
||||
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
|
||||
.collect();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("encode", size), size, |bench, _| {
|
||||
bench.iter(|| BinaryQuantized::quantize(black_box(&vector)));
|
||||
});
|
||||
|
||||
let quantized = BinaryQuantized::quantize(&vector);
|
||||
group.bench_with_input(BenchmarkId::new("decode", size), size, |bench, _| {
|
||||
bench.iter(|| quantized.reconstruct());
|
||||
});
|
||||
|
||||
let quantized2 = BinaryQuantized::quantize(&vector);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("hamming_distance", size),
|
||||
size,
|
||||
|bench, _| {
|
||||
bench.iter(|| quantized.distance(black_box(&quantized2)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_quantization_compression_ratio(c: &mut Criterion) {
|
||||
let dimensions = 384;
|
||||
let vector: Vec<f32> = (0..dimensions).map(|i| i as f32 * 0.01).collect();
|
||||
|
||||
c.bench_function("scalar_vs_binary_encoding", |b| {
|
||||
b.iter(|| {
|
||||
let scalar = ScalarQuantized::quantize(black_box(&vector));
|
||||
let binary = BinaryQuantized::quantize(black_box(&vector));
|
||||
(scalar, binary)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_scalar_quantization,
|
||||
bench_binary_quantization,
|
||||
bench_quantization_compression_ratio
|
||||
);
|
||||
criterion_main!(benches);
|
||||
217
crates/ruvector-core/benches/real_benchmark.rs
Normal file
217
crates/ruvector-core/benches/real_benchmark.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
//! Real Benchmarks for RuVector Core
|
||||
//!
|
||||
//! These are ACTUAL performance measurements, not simulations.
|
||||
//! Run with: cargo bench -p ruvector-core --bench real_benchmark
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ruvector_core::types::{DbOptions, HnswConfig};
|
||||
use ruvector_core::{DistanceMetric, SearchQuery, VectorDB, VectorEntry};
|
||||
use tempfile::tempdir;
|
||||
|
||||
/// Generate random vectors for benchmarking
|
||||
fn generate_vectors(count: usize, dim: usize) -> Vec<Vec<f32>> {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
(0..dim)
|
||||
.map(|j| {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
(i * dim + j).hash(&mut hasher);
|
||||
let h = hasher.finish();
|
||||
((h % 2000) as f32 / 1000.0) - 1.0 // Range [-1, 1]
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Benchmark: Vector insertion (single)
|
||||
fn bench_insert_single(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("insert_single");
|
||||
|
||||
for dim in [64, 128, 256, 512].iter() {
|
||||
let vectors = generate_vectors(1000, *dim);
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::new("dimensions", dim), dim, |b, &dim| {
|
||||
let dir = tempdir().unwrap();
|
||||
let options = DbOptions {
|
||||
storage_path: dir.path().join("bench.db").to_string_lossy().to_string(),
|
||||
dimensions: dim,
|
||||
distance_metric: DistanceMetric::Cosine,
|
||||
hnsw_config: Some(HnswConfig::default()),
|
||||
quantization: None,
|
||||
};
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
let mut idx = 0;
|
||||
|
||||
b.iter(|| {
|
||||
let entry = VectorEntry {
|
||||
id: None,
|
||||
vector: vectors[idx % vectors.len()].clone(),
|
||||
metadata: None,
|
||||
};
|
||||
let _ = black_box(db.insert(entry));
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark: Vector insertion (batch)
|
||||
fn bench_insert_batch(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("insert_batch");
|
||||
|
||||
for batch_size in [100, 500, 1000].iter() {
|
||||
let vectors = generate_vectors(*batch_size, 128);
|
||||
|
||||
group.throughput(Throughput::Elements(*batch_size as u64));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("batch_size", batch_size),
|
||||
batch_size,
|
||||
|b, &batch_size| {
|
||||
b.iter(|| {
|
||||
let dir = tempdir().unwrap();
|
||||
let options = DbOptions {
|
||||
storage_path: dir.path().join("bench.db").to_string_lossy().to_string(),
|
||||
dimensions: 128,
|
||||
distance_metric: DistanceMetric::Cosine,
|
||||
hnsw_config: Some(HnswConfig::default()),
|
||||
quantization: None,
|
||||
};
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
|
||||
let entries: Vec<VectorEntry> = vectors
|
||||
.iter()
|
||||
.map(|v| VectorEntry {
|
||||
id: None,
|
||||
vector: v.clone(),
|
||||
metadata: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
black_box(db.insert_batch(entries).unwrap())
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark: Search (k-NN)
|
||||
fn bench_search(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("search");
|
||||
|
||||
// Pre-populate database
|
||||
let dir = tempdir().unwrap();
|
||||
let options = DbOptions {
|
||||
storage_path: dir.path().join("bench.db").to_string_lossy().to_string(),
|
||||
dimensions: 128,
|
||||
distance_metric: DistanceMetric::Cosine,
|
||||
hnsw_config: Some(HnswConfig {
|
||||
m: 16,
|
||||
ef_construction: 100,
|
||||
ef_search: 50,
|
||||
max_elements: 100000,
|
||||
}),
|
||||
quantization: None,
|
||||
};
|
||||
let db = VectorDB::new(options).unwrap();
|
||||
|
||||
// Insert 10k vectors
|
||||
let vectors = generate_vectors(10000, 128);
|
||||
let entries: Vec<VectorEntry> = vectors
|
||||
.iter()
|
||||
.map(|v| VectorEntry {
|
||||
id: None,
|
||||
vector: v.clone(),
|
||||
metadata: None,
|
||||
})
|
||||
.collect();
|
||||
db.insert_batch(entries).unwrap();
|
||||
|
||||
// Generate query vectors
|
||||
let queries = generate_vectors(100, 128);
|
||||
|
||||
for k in [10, 50, 100].iter() {
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::new("top_k", k), k, |b, &k| {
|
||||
let mut query_idx = 0;
|
||||
b.iter(|| {
|
||||
let query = &queries[query_idx % queries.len()];
|
||||
let search_query = SearchQuery {
|
||||
vector: query.clone(),
|
||||
k,
|
||||
filter: None,
|
||||
ef_search: None,
|
||||
};
|
||||
let results = black_box(db.search(search_query));
|
||||
query_idx += 1;
|
||||
results
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark: Distance computation (raw)
|
||||
fn bench_distance(c: &mut Criterion) {
|
||||
use ruvector_core::distance::{cosine_distance, dot_product_distance, euclidean_distance};
|
||||
|
||||
let mut group = c.benchmark_group("distance");
|
||||
|
||||
for dim in [64, 128, 256, 512, 1024].iter() {
|
||||
let v1: Vec<f32> = (0..*dim).map(|i| (i as f32 * 0.01).sin()).collect();
|
||||
let v2: Vec<f32> = (0..*dim).map(|i| (i as f32 * 0.02).cos()).collect();
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("cosine", dim), dim, |b, _| {
|
||||
b.iter(|| black_box(cosine_distance(&v1, &v2)));
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("euclidean", dim), dim, |b, _| {
|
||||
b.iter(|| black_box(euclidean_distance(&v1, &v2)));
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("dot_product", dim), dim, |b, _| {
|
||||
b.iter(|| black_box(dot_product_distance(&v1, &v2)));
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark: Quantization
|
||||
fn bench_quantization(c: &mut Criterion) {
|
||||
use ruvector_core::quantization::{QuantizedVector, ScalarQuantized};
|
||||
|
||||
let mut group = c.benchmark_group("quantization");
|
||||
|
||||
for dim in [128, 256, 512].iter() {
|
||||
let vector: Vec<f32> = (0..*dim).map(|i| (i as f32 * 0.01).sin()).collect();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("scalar_quantize", dim), dim, |b, _| {
|
||||
b.iter(|| black_box(ScalarQuantized::quantize(&vector)));
|
||||
});
|
||||
|
||||
let quantized = ScalarQuantized::quantize(&vector);
|
||||
group.bench_with_input(BenchmarkId::new("scalar_distance", dim), dim, |b, _| {
|
||||
b.iter(|| black_box(quantized.distance(&quantized)));
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_distance,
|
||||
bench_quantization,
|
||||
bench_insert_single,
|
||||
bench_insert_batch,
|
||||
bench_search,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
Reference in New Issue
Block a user