Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
248
crates/ruvector-mincut/benches/README.md
Normal file
248
crates/ruvector-mincut/benches/README.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# RuVector MinCut Benchmarks
|
||||
|
||||
Comprehensive benchmark suite for the dynamic minimum cut algorithm implementation.
|
||||
|
||||
## Overview
|
||||
|
||||
This benchmark suite measures the performance characteristics of the dynamic graph operations that underpin the minimum cut algorithm, including:
|
||||
|
||||
- **Edge Operations**: Insert/delete throughput and scaling
|
||||
- **Query Performance**: Connectivity, degree, edge existence checks
|
||||
- **Workload Patterns**: Mixed operations, batch processing
|
||||
- **Scaling Analysis**: Verification of subpolynomial update time (O(n^(2/3)))
|
||||
- **Graph Types**: Random, grid, complete, sparse, and dense graphs
|
||||
|
||||
## Running Benchmarks
|
||||
|
||||
### Run All Benchmarks
|
||||
|
||||
```bash
|
||||
cd crates/ruvector-mincut
|
||||
cargo bench --bench mincut_bench
|
||||
```
|
||||
|
||||
### Run Specific Benchmark Groups
|
||||
|
||||
```bash
|
||||
# Graph operations (insert, delete, queries)
|
||||
cargo bench --bench mincut_bench graph_ops
|
||||
|
||||
# Query operations
|
||||
cargo bench --bench mincut_bench queries
|
||||
|
||||
# Workload patterns
|
||||
cargo bench --bench mincut_bench workloads
|
||||
|
||||
# Scaling analysis
|
||||
cargo bench --bench mincut_bench scaling
|
||||
```
|
||||
|
||||
### Run Individual Benchmarks
|
||||
|
||||
```bash
|
||||
# Insert edge performance
|
||||
cargo bench --bench mincut_bench -- insert_edge
|
||||
|
||||
# Delete edge performance
|
||||
cargo bench --bench mincut_bench -- delete_edge
|
||||
|
||||
# Mixed workload
|
||||
cargo bench --bench mincut_bench -- mixed_workload
|
||||
|
||||
# Scaling analysis
|
||||
cargo bench --bench mincut_bench -- scaling_analysis
|
||||
```
|
||||
|
||||
### Filter by Size
|
||||
|
||||
```bash
|
||||
# Benchmark only size 1000
|
||||
cargo bench --bench mincut_bench -- /1000
|
||||
|
||||
# Benchmark sizes 100 and 5000
|
||||
cargo bench --bench mincut_bench -- "/100|5000"
|
||||
```
|
||||
|
||||
## Benchmark Groups
|
||||
|
||||
### 1. Graph Operations (`graph_ops`)
|
||||
|
||||
- **`insert_edge`**: Edge insertion throughput at various graph sizes (100 to 10K vertices)
|
||||
- **`delete_edge`**: Edge deletion throughput
|
||||
- **`degree_query`**: Vertex degree query performance
|
||||
- **`has_edge_query`**: Edge existence check performance
|
||||
|
||||
### 2. Query Operations (`queries`)
|
||||
|
||||
- **`query_connectivity`**: Graph connectivity checking (BFS-based)
|
||||
- **`stats_computation`**: Graph statistics calculation
|
||||
- **`connected_components`**: Connected components computation
|
||||
- **`neighbors_iteration`**: Neighbor list retrieval
|
||||
|
||||
### 3. Workload Patterns (`workloads`)
|
||||
|
||||
- **`mixed_workload`**: Realistic mixed operations (50% insert, 30% delete, 20% query)
|
||||
- **`batch_operations`**: Batch insertion performance (10 to 1000 edges)
|
||||
|
||||
### 4. Scaling Analysis (`scaling`)
|
||||
|
||||
- **`scaling_analysis`**: Verify subpolynomial O(n^(2/3)) update time
|
||||
- Tests sizes: 100, 316, 1K, 3.2K, 10K vertices
|
||||
- Both insert and delete operations
|
||||
- **`graph_types`**: Performance across different graph structures
|
||||
- Random graphs
|
||||
- Grid graphs (structured)
|
||||
- Complete graphs (dense)
|
||||
- Sparse graphs (avg degree ~4)
|
||||
- Dense graphs (~30% edge density)
|
||||
- **`memory_efficiency`**: Graph creation and memory footprint
|
||||
|
||||
## Graph Generators
|
||||
|
||||
The benchmark suite includes several graph generators:
|
||||
|
||||
- **Random Graph**: `generate_random_graph(n, m, seed)` - n vertices, m random edges
|
||||
- **Grid Graph**: `generate_grid_graph(width, height)` - Structured 2D grid
|
||||
- **Complete Graph**: `generate_complete_graph(n)` - All possible edges
|
||||
- **Sparse Graph**: `generate_sparse_graph(n, seed)` - Average degree ~4
|
||||
- **Dense Graph**: `generate_dense_graph(n, seed)` - ~30% edge density
|
||||
|
||||
## Expected Performance Characteristics
|
||||
|
||||
Based on the theoretical analysis:
|
||||
|
||||
### Update Operations (Insert/Delete)
|
||||
- **Time Complexity**: O(n^(2/3)) amortized
|
||||
- **Scaling**: Should show subpolynomial growth
|
||||
- **Throughput**: Higher for smaller graphs
|
||||
|
||||
### Query Operations
|
||||
- **Connectivity**: O(n + m) via BFS
|
||||
- **Degree**: O(1) via adjacency list lookup
|
||||
- **Has Edge**: O(1) via hash table lookup
|
||||
- **Connected Components**: O(n + m) via BFS
|
||||
|
||||
### Graph Types
|
||||
- **Sparse graphs**: Better cache locality, faster updates
|
||||
- **Dense graphs**: More edges to maintain, slower updates
|
||||
- **Grid graphs**: Good locality, predictable performance
|
||||
- **Complete graphs**: Maximum edges, highest overhead
|
||||
|
||||
## Interpreting Results
|
||||
|
||||
### Scaling Verification
|
||||
|
||||
The `scaling_analysis` benchmarks test sizes: 100, 316, 1000, 3162, 10000
|
||||
|
||||
For O(n^(2/3)) complexity:
|
||||
- 100 → 316 (3.16x): expect ~2.1x slowdown
|
||||
- 316 → 1000 (3.16x): expect ~2.1x slowdown
|
||||
- 1000 → 3162 (3.16x): expect ~2.1x slowdown
|
||||
- 3162 → 10000 (3.16x): expect ~2.1x slowdown
|
||||
|
||||
If you observe linear or worse scaling, there may be algorithmic issues.
|
||||
|
||||
### Throughput Metrics
|
||||
|
||||
Criterion reports:
|
||||
- **Time/iteration**: Lower is better
|
||||
- **Throughput**: Higher is better
|
||||
- **R²**: Close to 1.0 indicates stable measurements
|
||||
|
||||
### Performance Baseline
|
||||
|
||||
Typical results on modern hardware (approximate):
|
||||
- Small graphs (100-500): ~100-500 ns/operation
|
||||
- Medium graphs (1K-5K): ~500ns-2μs/operation
|
||||
- Large graphs (10K+): ~2-10μs/operation
|
||||
|
||||
## Customizing Benchmarks
|
||||
|
||||
### Adjust Sample Sizes
|
||||
|
||||
Modify `group.sample_size()` in the benchmark code:
|
||||
|
||||
```rust
|
||||
let mut group = c.benchmark_group("my_group");
|
||||
group.sample_size(50); // Default is 100
|
||||
```
|
||||
|
||||
### Add Custom Graph Sizes
|
||||
|
||||
Edit the size arrays in each benchmark:
|
||||
|
||||
```rust
|
||||
for size in [100, 500, 1000, 2500, 5000, 10000].iter() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Change Workload Mix
|
||||
|
||||
Modify the `bench_mixed_workload` ratios:
|
||||
|
||||
```rust
|
||||
match op {
|
||||
0..=5 => { /* 60% inserts */ },
|
||||
6..=8 => { /* 30% deletes */ },
|
||||
_ => { /* 10% queries */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Benchmark results are saved to:
|
||||
- `target/criterion/`: HTML reports with graphs
|
||||
- `target/criterion/{benchmark_name}/report/index.html`: Detailed reports
|
||||
|
||||
Open in browser:
|
||||
```bash
|
||||
open target/criterion/report/index.html
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
Add to CI pipeline:
|
||||
|
||||
```yaml
|
||||
- name: Run benchmarks
|
||||
run: |
|
||||
cd crates/ruvector-mincut
|
||||
cargo bench --bench mincut_bench -- --output-format bencher | tee bench_output.txt
|
||||
```
|
||||
|
||||
Compare against baseline:
|
||||
```bash
|
||||
cargo install critcmp
|
||||
critcmp baseline current
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Benchmarks Take Too Long
|
||||
|
||||
Reduce sample size or iterations:
|
||||
```rust
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
```
|
||||
|
||||
### Unstable Results
|
||||
|
||||
- Close other applications
|
||||
- Disable CPU frequency scaling
|
||||
- Run with `nice -n -20` for higher priority
|
||||
- Check system load: `uptime`
|
||||
|
||||
### Out of Memory
|
||||
|
||||
Reduce graph sizes:
|
||||
```rust
|
||||
for size in [100, 500, 1000].iter() { // Skip 5K and 10K
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Criterion.rs Documentation](https://bheisler.github.io/criterion.rs/book/)
|
||||
- Dynamic Min-Cut Algorithm: O(n^(2/3)) update time
|
||||
- Graph Connectivity: BFS in O(n + m)
|
||||
496
crates/ruvector-mincut/benches/bounded_bench.rs
Normal file
496
crates/ruvector-mincut/benches/bounded_bench.rs
Normal file
@@ -0,0 +1,496 @@
|
||||
//! Benchmarks for bounded-range dynamic minimum cut
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use ruvector_mincut::prelude::*;
|
||||
use ruvector_mincut::wrapper::MinCutWrapper;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Generate a random graph with n vertices and m edges
|
||||
fn generate_random_edges(n: usize, m: usize, seed: u64) -> Vec<(u64, u64)> {
|
||||
let mut rng = StdRng::seed_from_u64(seed);
|
||||
let mut edges = Vec::with_capacity(m);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
|
||||
while edges.len() < m {
|
||||
let u = rng.gen_range(0..n as u64);
|
||||
let v = rng.gen_range(0..n as u64);
|
||||
if u != v {
|
||||
let key = if u < v { (u, v) } else { (v, u) };
|
||||
if seen.insert(key) {
|
||||
edges.push((u, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate path graph edges
|
||||
fn generate_path_edges(n: usize) -> Vec<(u64, u64)> {
|
||||
(0..n as u64 - 1).map(|i| (i, i + 1)).collect()
|
||||
}
|
||||
|
||||
/// Generate cycle graph edges
|
||||
fn generate_cycle_edges(n: usize) -> Vec<(u64, u64)> {
|
||||
let mut edges: Vec<_> = (0..n as u64 - 1).map(|i| (i, i + 1)).collect();
|
||||
edges.push((n as u64 - 1, 0));
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate complete graph edges
|
||||
fn generate_complete_edges(n: usize) -> Vec<(u64, u64)> {
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..n as u64 {
|
||||
for j in (i + 1)..n as u64 {
|
||||
edges.push((i, j));
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate grid graph edges
|
||||
fn generate_grid_edges(width: usize, height: usize) -> Vec<(u64, u64)> {
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..height {
|
||||
for j in 0..width {
|
||||
let v = (i * width + j) as u64;
|
||||
if j + 1 < width {
|
||||
edges.push((v, v + 1));
|
||||
}
|
||||
if i + 1 < height {
|
||||
edges.push((v, v + (width as u64)));
|
||||
}
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
fn benchmark_insert_edge(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("bounded_insert_edge");
|
||||
|
||||
for &size in &[100, 500, 1000, 5000] {
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
// Setup: create graph and wrapper with existing edges
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_path_edges(size);
|
||||
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
// Prepare new edge to insert
|
||||
let new_u = 0;
|
||||
let new_v = size as u64 / 2;
|
||||
|
||||
(graph, wrapper, new_u, new_v, size as u64)
|
||||
},
|
||||
|(graph, mut wrapper, u, v, edge_id)| {
|
||||
// Benchmark: single insert operation
|
||||
if graph.insert_edge(u, v, 1.0).is_ok() {
|
||||
wrapper.insert_edge(edge_id, u, v);
|
||||
}
|
||||
black_box(wrapper)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_delete_edge(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("bounded_delete_edge");
|
||||
|
||||
for &size in &[100, 500, 1000] {
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
// Setup: create graph with cycle so deletion doesn't disconnect
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_cycle_edges(size);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
// Edge to delete
|
||||
let del_u = 0;
|
||||
let del_v = 1;
|
||||
|
||||
(graph, wrapper, del_u, del_v)
|
||||
},
|
||||
|(graph, mut wrapper, u, v)| {
|
||||
// Benchmark: single delete operation
|
||||
wrapper.delete_edge(0, u, v);
|
||||
black_box(wrapper)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_query(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("bounded_query");
|
||||
|
||||
for &size in &[100, 500, 1000, 5000] {
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
// Setup: build path graph
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_path_edges(size);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
// Benchmark: query operation
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_query_after_updates(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("bounded_query_after_updates");
|
||||
|
||||
for &num_updates in &[10, 50, 100, 500] {
|
||||
group.throughput(Throughput::Elements(num_updates as u64));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(num_updates),
|
||||
&num_updates,
|
||||
|b, &num_updates| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
// Setup: build base graph
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let base_size = 500;
|
||||
let edges = generate_path_edges(base_size);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
// Add buffered updates
|
||||
let mut rng = StdRng::seed_from_u64(42);
|
||||
let mut edge_id = base_size as u64;
|
||||
|
||||
for _ in 0..num_updates {
|
||||
let u = rng.gen_range(0..base_size as u64);
|
||||
let v = rng.gen_range(0..base_size as u64);
|
||||
if u != v && graph.insert_edge(u, v, 1.0).is_ok() {
|
||||
wrapper.insert_edge(edge_id, u, v);
|
||||
edge_id += 1;
|
||||
}
|
||||
}
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
// Benchmark: query with buffered updates
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_different_topologies(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("bounded_topologies");
|
||||
let size = 500;
|
||||
|
||||
// Path graph
|
||||
group.bench_function("path", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_path_edges(size);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Cycle graph
|
||||
group.bench_function("cycle", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_cycle_edges(size);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Grid graph
|
||||
let grid_size = 22; // ~484 vertices
|
||||
group.bench_function("grid", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_grid_edges(grid_size, grid_size);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Complete graph (small due to O(n^2) edges)
|
||||
let complete_size = 30;
|
||||
group.bench_function("complete", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_complete_edges(complete_size);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_mixed_workload(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("bounded_mixed_workload");
|
||||
|
||||
group.bench_function("realistic_workload", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let base_size = 1000;
|
||||
let edges = generate_random_edges(base_size, base_size * 2, 42);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
(graph, wrapper)
|
||||
},
|
||||
|(graph, mut wrapper)| {
|
||||
let mut rng = StdRng::seed_from_u64(12345);
|
||||
let mut edge_id = 2000u64;
|
||||
|
||||
// Simulate realistic workload:
|
||||
// 70% queries, 20% inserts, 10% deletes
|
||||
for _ in 0..100 {
|
||||
let op = rng.gen_range(0..10);
|
||||
|
||||
if op < 7 {
|
||||
// Query
|
||||
let _ = black_box(wrapper.query());
|
||||
} else if op < 9 {
|
||||
// Insert
|
||||
let u = rng.gen_range(0..1000);
|
||||
let v = rng.gen_range(0..1000);
|
||||
if u != v && graph.insert_edge(u, v, 1.0).is_ok() {
|
||||
wrapper.insert_edge(edge_id, u, v);
|
||||
edge_id += 1;
|
||||
}
|
||||
} else {
|
||||
// Delete (try to delete a random edge)
|
||||
let u = rng.gen_range(0..1000);
|
||||
let v = rng.gen_range(0..1000);
|
||||
if graph.delete_edge(u, v).is_ok() {
|
||||
wrapper.delete_edge(edge_id, u, v);
|
||||
edge_id += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
black_box((graph, wrapper))
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_lazy_instantiation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("bounded_lazy_instantiation");
|
||||
|
||||
group.bench_function("first_query", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
// Setup: wrapper with no instances created yet
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_path_edges(500);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
assert_eq!(wrapper.num_instances(), 0, "No instances before query");
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
// Benchmark: first query triggers instantiation
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.bench_function("subsequent_query", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
// Setup: wrapper with instances already created
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_path_edges(500);
|
||||
|
||||
for (u, v) in &edges {
|
||||
graph.insert_edge(*u, *v, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
// Trigger initial instantiation
|
||||
let _ = wrapper.query();
|
||||
assert!(
|
||||
wrapper.num_instances() > 0,
|
||||
"Instances created after first query"
|
||||
);
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
// Benchmark: subsequent query (instances already exist)
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
benchmark_insert_edge,
|
||||
benchmark_delete_edge,
|
||||
benchmark_query,
|
||||
benchmark_query_after_updates,
|
||||
benchmark_different_topologies,
|
||||
benchmark_mixed_workload,
|
||||
benchmark_lazy_instantiation,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
1383
crates/ruvector-mincut/benches/jtree_bench.rs
Normal file
1383
crates/ruvector-mincut/benches/jtree_bench.rs
Normal file
File diff suppressed because it is too large
Load Diff
547
crates/ruvector-mincut/benches/mincut_bench.rs
Normal file
547
crates/ruvector-mincut/benches/mincut_bench.rs
Normal file
@@ -0,0 +1,547 @@
|
||||
//! Benchmarks for Dynamic Minimum Cut Algorithm
|
||||
//!
|
||||
//! Measures:
|
||||
//! - Insert/delete throughput at various graph sizes
|
||||
//! - Query latency
|
||||
//! - Scaling behavior (subpolynomial verification)
|
||||
//! - Comparison with static algorithms
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use rand::prelude::*;
|
||||
use ruvector_mincut::graph::DynamicGraph;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Generate a random graph with n vertices and m edges
|
||||
fn generate_random_graph(n: usize, m: usize, seed: u64) -> Vec<(u64, u64, f64)> {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
|
||||
let mut edges = Vec::with_capacity(m);
|
||||
let mut edge_set = HashSet::new();
|
||||
|
||||
while edges.len() < m {
|
||||
let u = rng.gen_range(0..n as u64);
|
||||
let v = rng.gen_range(0..n as u64);
|
||||
if u != v {
|
||||
let key = if u < v { (u, v) } else { (v, u) };
|
||||
if edge_set.insert(key) {
|
||||
edges.push((u, v, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate a grid graph (good test case with known min cuts)
|
||||
fn generate_grid_graph(width: usize, height: usize) -> Vec<(u64, u64, f64)> {
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..height {
|
||||
for j in 0..width {
|
||||
let v = (i * width + j) as u64;
|
||||
if j + 1 < width {
|
||||
edges.push((v, v + 1, 1.0));
|
||||
}
|
||||
if i + 1 < height {
|
||||
edges.push((v, v + width as u64, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate a complete graph with n vertices
|
||||
fn generate_complete_graph(n: usize) -> Vec<(u64, u64, f64)> {
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..n as u64 {
|
||||
for j in (i + 1)..n as u64 {
|
||||
edges.push((i, j, 1.0));
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate a sparse graph (average degree ~4)
|
||||
fn generate_sparse_graph(n: usize, seed: u64) -> Vec<(u64, u64, f64)> {
|
||||
let m = n * 2; // Average degree of 4
|
||||
generate_random_graph(n, m, seed)
|
||||
}
|
||||
|
||||
/// Generate a dense graph (edge probability ~0.3)
|
||||
fn generate_dense_graph(n: usize, seed: u64) -> Vec<(u64, u64, f64)> {
|
||||
let m = (n * (n - 1)) / 6; // ~30% of possible edges
|
||||
generate_random_graph(n, m, seed)
|
||||
}
|
||||
|
||||
/// Benchmark edge insertion throughput
|
||||
fn bench_insert_edge(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("insert_edge");
|
||||
|
||||
for size in [100, 500, 1000, 5000, 10000].iter() {
|
||||
let edges = generate_random_graph(*size, size * 2, 42);
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = DynamicGraph::with_capacity(size, size * 3);
|
||||
// Pre-populate with initial edges
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
(graph, rand::rngs::StdRng::seed_from_u64(123))
|
||||
},
|
||||
|(graph, mut rng)| {
|
||||
let u = rng.gen_range(0..size as u64);
|
||||
let v = rng.gen_range(0..size as u64);
|
||||
if u != v && !graph.has_edge(u, v) {
|
||||
let _ = black_box(graph.insert_edge(u, v, 1.0));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark edge deletion throughput
|
||||
fn bench_delete_edge(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("delete_edge");
|
||||
|
||||
for size in [100, 500, 1000, 5000, 10000].iter() {
|
||||
let edges = generate_random_graph(*size, size * 2, 42);
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = DynamicGraph::with_capacity(*size, size * 3);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
graph
|
||||
},
|
||||
|graph| {
|
||||
let edges_list = graph.edges();
|
||||
if !edges_list.is_empty() {
|
||||
let idx = rand::thread_rng().gen_range(0..edges_list.len());
|
||||
let edge = edges_list[idx];
|
||||
let _ = black_box(graph.delete_edge(edge.source, edge.target));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark query operations (connectivity checks)
|
||||
fn bench_query(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("query_connectivity");
|
||||
|
||||
for size in [100, 500, 1000, 5000, 10000].iter() {
|
||||
let edges = generate_random_graph(*size, size * 2, 42);
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
let graph = DynamicGraph::with_capacity(*size, size * 3);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
|
||||
b.iter(|| black_box(graph.is_connected()));
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark degree queries
|
||||
fn bench_degree_query(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("query_degree");
|
||||
|
||||
for size in [100, 500, 1000, 5000, 10000].iter() {
|
||||
let edges = generate_random_graph(*size, size * 2, 42);
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||
let graph = DynamicGraph::with_capacity(size, size * 3);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(456);
|
||||
b.iter(|| {
|
||||
let v = rng.gen_range(0..size as u64);
|
||||
black_box(graph.degree(v))
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark edge existence queries
|
||||
fn bench_has_edge_query(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("query_has_edge");
|
||||
|
||||
for size in [100, 500, 1000, 5000, 10000].iter() {
|
||||
let edges = generate_random_graph(*size, size * 2, 42);
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||
let graph = DynamicGraph::with_capacity(size, size * 3);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(789);
|
||||
b.iter(|| {
|
||||
let u = rng.gen_range(0..size as u64);
|
||||
let v = rng.gen_range(0..size as u64);
|
||||
black_box(graph.has_edge(u, v))
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark mixed workload: 50% inserts, 30% deletes, 20% queries
|
||||
fn bench_mixed_workload(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("mixed_workload");
|
||||
|
||||
for size in [100, 500, 1000, 5000].iter() {
|
||||
let initial_edges = generate_random_graph(*size, size * 2, 42);
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = DynamicGraph::with_capacity(size, size * 3);
|
||||
for (u, v, w) in &initial_edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
(graph, rand::rngs::StdRng::seed_from_u64(999))
|
||||
},
|
||||
|(graph, mut rng)| {
|
||||
let op = rng.gen_range(0..10);
|
||||
|
||||
match op {
|
||||
// 50% inserts (0-4)
|
||||
0..=4 => {
|
||||
let u = rng.gen_range(0..size as u64);
|
||||
let v = rng.gen_range(0..size as u64);
|
||||
if u != v && !graph.has_edge(u, v) {
|
||||
let _ = graph.insert_edge(u, v, 1.0);
|
||||
}
|
||||
}
|
||||
// 30% deletes (5-7)
|
||||
5..=7 => {
|
||||
let edges_list = graph.edges();
|
||||
if !edges_list.is_empty() {
|
||||
let idx = rng.gen_range(0..edges_list.len());
|
||||
let edge = edges_list[idx];
|
||||
let _ = graph.delete_edge(edge.source, edge.target);
|
||||
}
|
||||
}
|
||||
// 20% queries (8-9)
|
||||
_ => {
|
||||
let u = rng.gen_range(0..size as u64);
|
||||
let _ = black_box(graph.degree(u));
|
||||
}
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark scaling behavior - verify subpolynomial update time
|
||||
fn bench_scaling(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("scaling_analysis");
|
||||
group.sample_size(20); // Reduce sample size for larger graphs
|
||||
|
||||
// Test sizes chosen to show subpolynomial scaling: n^(2/3)
|
||||
let sizes = vec![100, 316, 1000, 3162, 10000];
|
||||
|
||||
for size in sizes {
|
||||
let edges = generate_random_graph(size, size * 2, 42);
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("insert_scaling", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = DynamicGraph::with_capacity(size, size * 3);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
(graph, rand::rngs::StdRng::seed_from_u64(111))
|
||||
},
|
||||
|(graph, mut rng)| {
|
||||
let u = rng.gen_range(0..size as u64);
|
||||
let v = rng.gen_range(0..size as u64);
|
||||
if u != v && !graph.has_edge(u, v) {
|
||||
let _ = black_box(graph.insert_edge(u, v, 1.0));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("delete_scaling", size), &size, |b, _| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = DynamicGraph::with_capacity(size, size * 3);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
graph
|
||||
},
|
||||
|graph| {
|
||||
let edges_list = graph.edges();
|
||||
if !edges_list.is_empty() {
|
||||
let idx = rand::thread_rng().gen_range(0..edges_list.len());
|
||||
let edge = edges_list[idx];
|
||||
let _ = black_box(graph.delete_edge(edge.source, edge.target));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark different graph types
|
||||
fn bench_graph_types(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("graph_types");
|
||||
let size = 1000;
|
||||
|
||||
// Random graph
|
||||
let random_edges = generate_random_graph(size, size * 2, 42);
|
||||
group.bench_function("random_insert", |b| {
|
||||
b.iter_batched(
|
||||
|| DynamicGraph::with_capacity(size, size * 3),
|
||||
|graph| {
|
||||
for (u, v, w) in &random_edges {
|
||||
let _ = black_box(graph.insert_edge(*u, *v, *w));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Grid graph (31x32 ≈ 1000 vertices)
|
||||
let grid_edges = generate_grid_graph(31, 32);
|
||||
group.bench_function("grid_insert", |b| {
|
||||
b.iter_batched(
|
||||
|| DynamicGraph::with_capacity(size, size * 2),
|
||||
|graph| {
|
||||
for (u, v, w) in &grid_edges {
|
||||
let _ = black_box(graph.insert_edge(*u, *v, *w));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Complete graph (45 vertices ≈ 990 edges)
|
||||
let complete_edges = generate_complete_graph(45);
|
||||
group.bench_function("complete_insert", |b| {
|
||||
b.iter_batched(
|
||||
|| DynamicGraph::with_capacity(45, 1000),
|
||||
|graph| {
|
||||
for (u, v, w) in &complete_edges {
|
||||
let _ = black_box(graph.insert_edge(*u, *v, *w));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Sparse graph
|
||||
let sparse_edges = generate_sparse_graph(size, 42);
|
||||
group.bench_function("sparse_insert", |b| {
|
||||
b.iter_batched(
|
||||
|| DynamicGraph::with_capacity(size, size * 3),
|
||||
|graph| {
|
||||
for (u, v, w) in &sparse_edges {
|
||||
let _ = black_box(graph.insert_edge(*u, *v, *w));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Dense graph
|
||||
let dense_edges = generate_dense_graph(size, 42);
|
||||
group.bench_function("dense_insert", |b| {
|
||||
b.iter_batched(
|
||||
|| DynamicGraph::with_capacity(size, dense_edges.len() + 100),
|
||||
|graph| {
|
||||
for (u, v, w) in &dense_edges {
|
||||
let _ = black_box(graph.insert_edge(*u, *v, *w));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark graph statistics computation
|
||||
fn bench_stats(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("stats_computation");
|
||||
|
||||
for size in [100, 500, 1000, 5000].iter() {
|
||||
let edges = generate_random_graph(*size, size * 2, 42);
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
let graph = DynamicGraph::with_capacity(*size, size * 3);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
|
||||
b.iter(|| black_box(graph.stats()));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark connected components computation
|
||||
fn bench_connected_components(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("connected_components");
|
||||
|
||||
for size in [100, 500, 1000, 5000].iter() {
|
||||
// Create a graph with multiple components
|
||||
let mut edges = Vec::new();
|
||||
let component_size = size / 5; // 5 components
|
||||
|
||||
for comp in 0..5 {
|
||||
let offset = comp * component_size;
|
||||
for i in 0..component_size - 1 {
|
||||
let u = (offset + i) as u64;
|
||||
let v = (offset + i + 1) as u64;
|
||||
edges.push((u, v, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
let graph = DynamicGraph::with_capacity(*size, edges.len());
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
|
||||
b.iter(|| black_box(graph.connected_components()));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark neighbor iteration
|
||||
fn bench_neighbors(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("neighbors_iteration");
|
||||
|
||||
for degree in [10, 50, 100, 500, 1000].iter() {
|
||||
// Create a star graph with one high-degree vertex
|
||||
let mut edges = Vec::new();
|
||||
for i in 1..=*degree {
|
||||
edges.push((0, i as u64, 1.0));
|
||||
}
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(degree), degree, |b, _| {
|
||||
let graph = DynamicGraph::with_capacity(*degree + 1, *degree);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
|
||||
b.iter(|| black_box(graph.neighbors(0)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark batch operations
|
||||
fn bench_batch_operations(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("batch_operations");
|
||||
|
||||
for batch_size in [10, 50, 100, 500, 1000].iter() {
|
||||
let edges = generate_random_graph(5000, *batch_size, 42);
|
||||
|
||||
group.throughput(Throughput::Elements(*batch_size as u64));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("batch_insert", batch_size),
|
||||
batch_size,
|
||||
|b, _| {
|
||||
b.iter_batched(
|
||||
|| DynamicGraph::with_capacity(5000, *batch_size + 100),
|
||||
|graph| {
|
||||
for (u, v, w) in &edges {
|
||||
let _ = black_box(graph.insert_edge(*u, *v, *w));
|
||||
}
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark memory footprint (using num_edges as proxy)
|
||||
fn bench_memory_efficiency(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("memory_efficiency");
|
||||
|
||||
for size in [1000, 5000, 10000].iter() {
|
||||
let edges = generate_random_graph(*size, size * 2, 42);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("graph_creation", size), size, |b, _| {
|
||||
b.iter(|| {
|
||||
let graph = DynamicGraph::with_capacity(*size, size * 3);
|
||||
for (u, v, w) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, *w);
|
||||
}
|
||||
black_box(graph)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
graph_ops,
|
||||
bench_insert_edge,
|
||||
bench_delete_edge,
|
||||
bench_degree_query,
|
||||
bench_has_edge_query,
|
||||
);
|
||||
|
||||
criterion_group!(
|
||||
queries,
|
||||
bench_query,
|
||||
bench_stats,
|
||||
bench_connected_components,
|
||||
bench_neighbors,
|
||||
);
|
||||
|
||||
criterion_group!(workloads, bench_mixed_workload, bench_batch_operations,);
|
||||
|
||||
criterion_group!(
|
||||
scaling,
|
||||
bench_scaling,
|
||||
bench_graph_types,
|
||||
bench_memory_efficiency,
|
||||
);
|
||||
|
||||
criterion_main!(graph_ops, queries, workloads, scaling);
|
||||
368
crates/ruvector-mincut/benches/optimization_bench.rs
Normal file
368
crates/ruvector-mincut/benches/optimization_bench.rs
Normal file
@@ -0,0 +1,368 @@
|
||||
//! Benchmark suite for j-Tree + BMSSP optimizations
|
||||
//!
|
||||
//! Measures before/after performance for each optimization:
|
||||
//! - DSpar: 5.9x target
|
||||
//! - Cache: 10x target
|
||||
//! - SIMD: 2-4x target
|
||||
//! - Pool: 50-75% memory reduction
|
||||
//! - Parallel: Near-linear scaling
|
||||
//! - WASM Batch: 10x FFI reduction
|
||||
//!
|
||||
//! Target: Combined 10x speedup
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ruvector_mincut::graph::DynamicGraph;
|
||||
use ruvector_mincut::optimization::{
|
||||
BatchConfig, BenchmarkSuite, CacheConfig, DegreePresparse, DistanceArray, LevelData, LevelPool,
|
||||
LevelUpdateResult, ParallelConfig, ParallelLevelUpdater, PathDistanceCache, PoolConfig,
|
||||
PresparseConfig, SimdDistanceOps, WasmBatchOps,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Create test graph with specified size
|
||||
fn create_test_graph(vertices: usize, edges: usize) -> DynamicGraph {
|
||||
let graph = DynamicGraph::new();
|
||||
|
||||
for i in 0..vertices {
|
||||
graph.add_vertex(i as u64);
|
||||
}
|
||||
|
||||
let mut edge_count = 0;
|
||||
for i in 0..vertices {
|
||||
for j in (i + 1)..vertices {
|
||||
if edge_count >= edges {
|
||||
break;
|
||||
}
|
||||
let _ = graph.insert_edge(i as u64, j as u64, 1.0);
|
||||
edge_count += 1;
|
||||
}
|
||||
if edge_count >= edges {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
|
||||
/// Benchmark DSpar sparsification
|
||||
fn bench_dspar(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("DSpar");
|
||||
|
||||
for size in [100, 1000, 5000].iter() {
|
||||
let graph = create_test_graph(*size, size * 5);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("baseline", size), size, |b, _| {
|
||||
b.iter(|| {
|
||||
let edges: Vec<_> = graph.edges().collect();
|
||||
black_box(edges.len())
|
||||
})
|
||||
});
|
||||
|
||||
let mut dspar = DegreePresparse::with_config(PresparseConfig {
|
||||
target_sparsity: 0.1,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("optimized", size), size, |b, _| {
|
||||
b.iter(|| {
|
||||
let result = dspar.presparse(&graph);
|
||||
black_box(result.edges.len())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark path distance cache
|
||||
fn bench_cache(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("PathCache");
|
||||
|
||||
for size in [100, 1000, 5000].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("baseline_no_cache", size),
|
||||
size,
|
||||
|b, &size| {
|
||||
b.iter(|| {
|
||||
let mut total = 0.0;
|
||||
for i in 0..size {
|
||||
total += (i as f64 * 1.414).sqrt();
|
||||
}
|
||||
black_box(total)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let cache = PathDistanceCache::with_config(CacheConfig {
|
||||
max_entries: *size,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Pre-populate cache
|
||||
for i in 0..*size {
|
||||
cache.insert(i as u64, (i + 1) as u64, (i as f64).sqrt());
|
||||
}
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("optimized_with_cache", size),
|
||||
size,
|
||||
|b, &size| {
|
||||
b.iter(|| {
|
||||
let mut total = 0.0;
|
||||
for i in 0..size {
|
||||
if let Some(d) = cache.get(i as u64, (i + 1) as u64) {
|
||||
total += d;
|
||||
}
|
||||
}
|
||||
black_box(total)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark SIMD distance operations
|
||||
fn bench_simd(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("SIMD");
|
||||
|
||||
for size in [100, 1000, 10000].iter() {
|
||||
let mut arr = DistanceArray::new(*size);
|
||||
for i in 0..*size {
|
||||
arr.set(i as u64, (i as f64) * 0.5 + 1.0);
|
||||
}
|
||||
arr.set((*size / 2) as u64, 0.1);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("find_min_naive", size), &arr, |b, arr| {
|
||||
b.iter(|| {
|
||||
let data = arr.as_slice();
|
||||
let mut min_val = f64::INFINITY;
|
||||
let mut min_idx = 0;
|
||||
for (i, &d) in data.iter().enumerate() {
|
||||
if d < min_val {
|
||||
min_val = d;
|
||||
min_idx = i;
|
||||
}
|
||||
}
|
||||
black_box((min_val, min_idx))
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("find_min_simd", size), &arr, |b, arr| {
|
||||
b.iter(|| black_box(SimdDistanceOps::find_min(arr)))
|
||||
});
|
||||
|
||||
let neighbors: Vec<_> = (0..(size / 10).min(100))
|
||||
.map(|i| ((i * 10) as u64, 1.0))
|
||||
.collect();
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("relax_batch_naive", size),
|
||||
size,
|
||||
|b, &size| {
|
||||
let mut arr = DistanceArray::new(size);
|
||||
b.iter(|| {
|
||||
let data = arr.as_mut_slice();
|
||||
for &(idx, weight) in &neighbors {
|
||||
let idx = idx as usize;
|
||||
if idx < data.len() {
|
||||
let new_dist = 0.0 + weight;
|
||||
if new_dist < data[idx] {
|
||||
data[idx] = new_dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
black_box(())
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("relax_batch_simd", size),
|
||||
size,
|
||||
|b, &size| {
|
||||
let mut arr = DistanceArray::new(size);
|
||||
b.iter(|| black_box(SimdDistanceOps::relax_batch(&mut arr, 0.0, &neighbors)))
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark pool allocation
|
||||
fn bench_pool(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Pool");
|
||||
|
||||
for size in [100, 1000].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("baseline_alloc_dealloc", size),
|
||||
size,
|
||||
|b, &size| {
|
||||
b.iter(|| {
|
||||
let mut levels = Vec::new();
|
||||
for i in 0..10 {
|
||||
levels.push(LevelData::new(i, size));
|
||||
}
|
||||
black_box(levels.len())
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let pool = LevelPool::with_config(PoolConfig {
|
||||
max_materialized_levels: 5,
|
||||
lazy_dealloc: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("optimized_pool", size),
|
||||
size,
|
||||
|b, &size| {
|
||||
b.iter(|| {
|
||||
for i in 0..10 {
|
||||
let level = pool.allocate_level(i, size);
|
||||
pool.materialize(i, level);
|
||||
}
|
||||
black_box(pool.stats().materialized_levels)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark parallel processing
|
||||
fn bench_parallel(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Parallel");
|
||||
|
||||
let levels: Vec<usize> = (0..100).collect();
|
||||
|
||||
for work_size in [10, 100, 1000].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("sequential", work_size),
|
||||
work_size,
|
||||
|b, &work_size| {
|
||||
b.iter(|| {
|
||||
let _results: Vec<_> = levels
|
||||
.iter()
|
||||
.map(|&level| {
|
||||
let mut sum = 0.0;
|
||||
for i in 0..work_size {
|
||||
sum += (i as f64).sqrt();
|
||||
}
|
||||
LevelUpdateResult {
|
||||
level,
|
||||
cut_value: sum,
|
||||
partition: HashSet::new(),
|
||||
time_us: 0,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
black_box(())
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let updater = ParallelLevelUpdater::with_config(ParallelConfig {
|
||||
min_parallel_size: 10,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("parallel_rayon", work_size),
|
||||
work_size,
|
||||
|b, &work_size| {
|
||||
b.iter(|| {
|
||||
let _results = updater.process_parallel(&levels, |level| {
|
||||
let mut sum = 0.0;
|
||||
for i in 0..work_size {
|
||||
sum += (i as f64).sqrt();
|
||||
}
|
||||
LevelUpdateResult {
|
||||
level,
|
||||
cut_value: sum,
|
||||
partition: HashSet::new(),
|
||||
time_us: 0,
|
||||
}
|
||||
});
|
||||
black_box(())
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark WASM batch operations
|
||||
fn bench_wasm_batch(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("WASM_Batch");
|
||||
|
||||
for size in [100, 1000, 5000].iter() {
|
||||
let edges: Vec<_> = (0..*size)
|
||||
.map(|i| (i as u64, (i + 1) as u64, 1.0))
|
||||
.collect();
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("individual_ops", size),
|
||||
&edges,
|
||||
|b, edges| {
|
||||
b.iter(|| {
|
||||
for edge in edges {
|
||||
black_box(edge);
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let mut batch = WasmBatchOps::with_config(BatchConfig {
|
||||
max_batch_size: 1024,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("batched_ops", size), &edges, |b, edges| {
|
||||
b.iter(|| {
|
||||
batch.queue_insert_edges(edges.clone());
|
||||
let results = batch.execute_batch();
|
||||
black_box(results.len())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Run complete benchmark suite
|
||||
fn bench_complete_suite(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Complete_Suite");
|
||||
|
||||
group.bench_function("full_optimization_suite", |b| {
|
||||
b.iter(|| {
|
||||
let mut suite = BenchmarkSuite::new()
|
||||
.with_sizes(vec![100])
|
||||
.with_iterations(1);
|
||||
|
||||
let results = suite.run_all();
|
||||
let combined = suite.combined_speedup();
|
||||
black_box((results.len(), combined))
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_dspar,
|
||||
bench_cache,
|
||||
bench_simd,
|
||||
bench_pool,
|
||||
bench_parallel,
|
||||
bench_wasm_batch,
|
||||
bench_complete_suite,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
305
crates/ruvector-mincut/benches/paper_algorithms_bench.rs
Normal file
305
crates/ruvector-mincut/benches/paper_algorithms_bench.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
//! Benchmarks for 2025 paper algorithm implementations
|
||||
//!
|
||||
//! Tests performance of:
|
||||
//! - PolylogConnectivity (arXiv:2510.08297)
|
||||
//! - ApproxMinCut (SODA 2025, arXiv:2412.15069)
|
||||
//! - CacheOptBFS
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ruvector_mincut::connectivity::cache_opt::{BatchProcessor, CacheOptAdjacency, CacheOptBFS};
|
||||
use ruvector_mincut::{
|
||||
ApproxMinCut, ApproxMinCutConfig, DynamicConnectivity, PolylogConnectivity, PolylogStats,
|
||||
};
|
||||
|
||||
/// Generate a random graph with n vertices and m edges
|
||||
fn generate_graph(n: usize, m: usize, seed: u64) -> Vec<(u64, u64)> {
|
||||
let mut edges = Vec::with_capacity(m);
|
||||
let mut rng = seed;
|
||||
|
||||
for _ in 0..m {
|
||||
// Simple LCG for deterministic "random" numbers
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let u = (rng % n as u64) as u64;
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let v = (rng % n as u64) as u64;
|
||||
if u != v {
|
||||
edges.push((u, v));
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate weighted graph edges
|
||||
fn generate_weighted_graph(n: usize, m: usize, seed: u64) -> Vec<(u64, u64, f64)> {
|
||||
let mut edges = Vec::with_capacity(m);
|
||||
let mut rng = seed;
|
||||
|
||||
for _ in 0..m {
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let u = (rng % n as u64) as u64;
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let v = (rng % n as u64) as u64;
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let w = ((rng % 100) as f64 + 1.0) / 10.0;
|
||||
if u != v {
|
||||
edges.push((u, v, w));
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PolylogConnectivity Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_polylog_insert(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("polylog_insert");
|
||||
|
||||
for size in [100, 500, 1000, 5000].iter() {
|
||||
let edges = generate_graph(*size, size * 2, 42);
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
b.iter(|| {
|
||||
let mut conn = PolylogConnectivity::new();
|
||||
for &(u, v) in &edges {
|
||||
conn.insert_edge(u, v);
|
||||
}
|
||||
black_box(conn.component_count())
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_polylog_delete(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("polylog_delete");
|
||||
|
||||
for size in [100, 500, 1000].iter() {
|
||||
let edges = generate_graph(*size, size * 2, 42);
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let mut conn = PolylogConnectivity::new();
|
||||
for &(u, v) in &edges {
|
||||
conn.insert_edge(u, v);
|
||||
}
|
||||
conn
|
||||
},
|
||||
|mut conn| {
|
||||
// Delete half the edges
|
||||
for i in 0..edges.len() / 2 {
|
||||
let (u, v) = edges[i];
|
||||
conn.delete_edge(u, v);
|
||||
}
|
||||
black_box(conn.component_count())
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_polylog_query(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("polylog_query");
|
||||
|
||||
for size in [100, 500, 1000, 5000].iter() {
|
||||
let edges = generate_graph(*size, size * 2, 42);
|
||||
let mut conn = PolylogConnectivity::new();
|
||||
for &(u, v) in &edges {
|
||||
conn.insert_edge(u, v);
|
||||
}
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||
let queries: Vec<(u64, u64)> = (0..100)
|
||||
.map(|i| {
|
||||
(
|
||||
(i * 7) as u64 % size as u64,
|
||||
(i * 13 + 1) as u64 % size as u64,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
b.iter(|| {
|
||||
let mut count = 0;
|
||||
for &(u, v) in &queries {
|
||||
if conn.connected(u, v) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
black_box(count)
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ApproxMinCut Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_approx_insert(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("approx_mincut_insert");
|
||||
|
||||
for size in [100, 500, 1000].iter() {
|
||||
let edges = generate_weighted_graph(*size, size * 2, 42);
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
b.iter(|| {
|
||||
let mut approx = ApproxMinCut::with_epsilon(0.1);
|
||||
for &(u, v, w) in &edges {
|
||||
approx.insert_edge(u, v, w);
|
||||
}
|
||||
black_box(approx.edge_count())
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_approx_query(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("approx_mincut_query");
|
||||
|
||||
for size in [50, 100, 200, 500].iter() {
|
||||
let edges = generate_weighted_graph(*size, size * 2, 42);
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let mut approx = ApproxMinCut::with_epsilon(0.1);
|
||||
for &(u, v, w) in &edges {
|
||||
approx.insert_edge(u, v, w);
|
||||
}
|
||||
approx
|
||||
},
|
||||
|mut approx| black_box(approx.min_cut_value()),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_approx_epsilon_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("approx_epsilon");
|
||||
let size = 200;
|
||||
let edges = generate_weighted_graph(size, size * 2, 42);
|
||||
|
||||
for epsilon in [0.05, 0.1, 0.2, 0.5].iter() {
|
||||
group.bench_with_input(BenchmarkId::from_parameter(epsilon), epsilon, |b, &eps| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let mut approx = ApproxMinCut::with_epsilon(eps);
|
||||
for &(u, v, w) in &edges {
|
||||
approx.insert_edge(u, v, w);
|
||||
}
|
||||
approx
|
||||
},
|
||||
|mut approx| black_box(approx.min_cut_value()),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CacheOptBFS Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_cache_opt_bfs(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cache_opt_bfs");
|
||||
|
||||
for size in [100, 500, 1000, 5000].iter() {
|
||||
let edges: Vec<(u64, u64, f64)> = generate_weighted_graph(*size, size * 3, 42);
|
||||
let max_v = edges
|
||||
.iter()
|
||||
.map(|(u, v, _)| (*u).max(*v))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let adj = CacheOptAdjacency::from_edges(&edges, max_v);
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
b.iter(|| {
|
||||
let bfs = CacheOptBFS::new(&adj, 0);
|
||||
black_box(bfs.run().len())
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_batch_processor(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("batch_processor");
|
||||
|
||||
for size in [100, 500, 1000, 5000].iter() {
|
||||
let edges: Vec<(u64, u64, f64)> = generate_weighted_graph(*size, size * 3, 42);
|
||||
let max_v = edges
|
||||
.iter()
|
||||
.map(|(u, v, _)| (*u).max(*v))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let adj = CacheOptAdjacency::from_edges(&edges, max_v);
|
||||
let vertices: Vec<u64> = (0..=max_v).collect();
|
||||
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, _| {
|
||||
let processor = BatchProcessor::new();
|
||||
b.iter(|| {
|
||||
let degrees = processor.compute_degrees(&adj, &vertices);
|
||||
black_box(degrees.len())
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Comparison: PolylogConnectivity vs DynamicConnectivity
|
||||
// ============================================================================
|
||||
|
||||
fn bench_connectivity_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("connectivity_comparison");
|
||||
|
||||
let size = 500;
|
||||
let edges = generate_graph(size, size * 2, 42);
|
||||
|
||||
// Polylog insert
|
||||
group.bench_function("polylog_build_500", |b| {
|
||||
b.iter(|| {
|
||||
let mut conn = PolylogConnectivity::new();
|
||||
for &(u, v) in &edges {
|
||||
conn.insert_edge(u, v);
|
||||
}
|
||||
black_box(conn.component_count())
|
||||
});
|
||||
});
|
||||
|
||||
// Standard DynamicConnectivity insert
|
||||
group.bench_function("dynamic_build_500", |b| {
|
||||
b.iter(|| {
|
||||
let mut conn = DynamicConnectivity::new();
|
||||
for &(u, v) in &edges {
|
||||
conn.insert_edge(u, v);
|
||||
}
|
||||
black_box(conn.component_count())
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_polylog_insert,
|
||||
bench_polylog_delete,
|
||||
bench_polylog_query,
|
||||
bench_approx_insert,
|
||||
bench_approx_query,
|
||||
bench_approx_epsilon_comparison,
|
||||
bench_cache_opt_bfs,
|
||||
bench_batch_processor,
|
||||
bench_connectivity_comparison,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
259
crates/ruvector-mincut/benches/snn_bench.rs
Normal file
259
crates/ruvector-mincut/benches/snn_bench.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
//! Benchmarks for SNN-MinCut Integration
|
||||
//!
|
||||
//! Measures:
|
||||
//! - LIF neuron step performance
|
||||
//! - STDP weight update throughput
|
||||
//! - Network propagation latency
|
||||
//! - Karger-Stein mincut performance
|
||||
//! - Full cognitive engine throughput
|
||||
//! - Synchrony computation efficiency
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ruvector_mincut::graph::DynamicGraph;
|
||||
use ruvector_mincut::snn::{
|
||||
compute_synchrony, AttractorConfig, AttractorDynamics, CognitiveMinCutEngine, EngineConfig,
|
||||
LIFNeuron, LayerConfig, NetworkConfig, NeuronConfig, Spike, SpikeTrain, SpikingNetwork,
|
||||
SynapseMatrix,
|
||||
};
|
||||
|
||||
/// Generate a random graph for benchmarking
|
||||
fn create_test_graph(n: usize) -> DynamicGraph {
|
||||
let graph = DynamicGraph::new();
|
||||
|
||||
// Add vertices
|
||||
for i in 0..n {
|
||||
graph.add_vertex(i as u64);
|
||||
}
|
||||
|
||||
// Add random edges (sparse graph ~3 edges per vertex)
|
||||
let mut seed: u64 = 12345;
|
||||
for i in 0..n {
|
||||
for _ in 0..3 {
|
||||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let j = (seed % n as u64) as usize;
|
||||
if i != j {
|
||||
let weight = ((seed % 100) as f64) / 100.0 + 0.5;
|
||||
let _ = graph.insert_edge(i as u64, j as u64, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
|
||||
/// Benchmark LIF neuron step
|
||||
fn bench_lif_neuron(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("lif_neuron");
|
||||
|
||||
for size in [100, 1000, 10000].iter() {
|
||||
group.throughput(Throughput::Elements(*size as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("step", size), size, |b, &size| {
|
||||
let config = NeuronConfig::default();
|
||||
let mut neurons: Vec<LIFNeuron> = (0..size)
|
||||
.map(|i| LIFNeuron::with_config(i, config.clone()))
|
||||
.collect();
|
||||
let currents: Vec<f64> = (0..size).map(|i| (i % 10) as f64 * 0.1).collect();
|
||||
|
||||
b.iter(|| {
|
||||
let mut spikes = 0;
|
||||
for (neuron, ¤t) in neurons.iter_mut().zip(currents.iter()) {
|
||||
if neuron.step(black_box(current), 1.0, 0.0) {
|
||||
spikes += 1;
|
||||
}
|
||||
}
|
||||
black_box(spikes)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark STDP weight updates
|
||||
fn bench_stdp(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("stdp");
|
||||
|
||||
for size in [100, 500, 1000].iter() {
|
||||
let n_synapses = size * size / 10; // ~10% connectivity
|
||||
group.throughput(Throughput::Elements(n_synapses as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("update", size), size, |b, &size| {
|
||||
let mut matrix = SynapseMatrix::new(size, size);
|
||||
|
||||
// Create sparse connections
|
||||
let mut seed: u64 = 42;
|
||||
for i in 0..size {
|
||||
for _ in 0..size / 10 {
|
||||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let j = (seed as usize) % size;
|
||||
matrix.add_synapse(i, j, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
// Simulate spike events
|
||||
for i in 0..size / 10 {
|
||||
matrix.on_pre_spike(i, black_box(i as f64));
|
||||
matrix.on_post_spike(i + 1, black_box(i as f64 + 5.0));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark spiking network propagation
|
||||
fn bench_network(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("spiking_network");
|
||||
|
||||
for &(input, hidden, output) in [(100, 50, 10), (500, 200, 50), (1000, 500, 100)].iter() {
|
||||
let total = input + hidden + output;
|
||||
group.throughput(Throughput::Elements(total as u64));
|
||||
|
||||
let name = format!("{}-{}-{}", input, hidden, output);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("step", &name),
|
||||
&(input, hidden, output),
|
||||
|b, &(i, h, o)| {
|
||||
let config = NetworkConfig {
|
||||
layers: vec![
|
||||
LayerConfig::new(i),
|
||||
LayerConfig::new(h),
|
||||
LayerConfig::new(o),
|
||||
],
|
||||
..NetworkConfig::default()
|
||||
};
|
||||
let mut network = SpikingNetwork::new(config);
|
||||
|
||||
b.iter(|| black_box(network.step()));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark attractor dynamics (includes Karger-Stein)
|
||||
fn bench_attractor(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("attractor_dynamics");
|
||||
group.sample_size(50); // Fewer samples for slower benchmarks
|
||||
|
||||
for size in [50, 100, 200].iter() {
|
||||
group.throughput(Throughput::Elements(*size as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("step", size), size, |b, &size| {
|
||||
let graph = create_test_graph(size);
|
||||
let config = AttractorConfig::default();
|
||||
let mut attractor = AttractorDynamics::new(graph, config);
|
||||
|
||||
b.iter(|| black_box(attractor.step()));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark synchrony computation
|
||||
fn bench_synchrony(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("synchrony");
|
||||
|
||||
for size in [100, 1000, 10000].iter() {
|
||||
group.throughput(Throughput::Elements(*size as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("compute", size), size, |b, &size| {
|
||||
// Generate random spikes
|
||||
let mut seed: u64 = 999;
|
||||
let spikes: Vec<Spike> = (0..size)
|
||||
.map(|i| {
|
||||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
Spike {
|
||||
neuron_id: (seed as usize) % 100,
|
||||
time: (i as f64) + ((seed % 100) as f64) / 100.0,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
b.iter(|| black_box(compute_synchrony(&spikes, 10.0)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark full cognitive engine
|
||||
fn bench_cognitive_engine(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cognitive_engine");
|
||||
group.sample_size(30); // Fewer samples for complex benchmark
|
||||
|
||||
for size in [50, 100].iter() {
|
||||
group.throughput(Throughput::Elements(*size as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("step", size), size, |b, &size| {
|
||||
let graph = create_test_graph(size);
|
||||
let config = EngineConfig::default();
|
||||
let mut engine = CognitiveMinCutEngine::new(graph, config);
|
||||
|
||||
b.iter(|| black_box(engine.step()));
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("run_10", size), size, |b, &size| {
|
||||
let graph = create_test_graph(size);
|
||||
let config = EngineConfig::default();
|
||||
let mut engine = CognitiveMinCutEngine::new(graph, config);
|
||||
|
||||
b.iter(|| black_box(engine.run(10)));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark spike train operations
|
||||
fn bench_spike_train(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("spike_train");
|
||||
|
||||
for size in [100, 1000, 5000].iter() {
|
||||
group.throughput(Throughput::Elements(*size as u64));
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("to_pattern", size), size, |b, &size| {
|
||||
let mut train = SpikeTrain::new(0);
|
||||
for i in 0..size {
|
||||
train.record_spike(i as f64 * 0.5);
|
||||
}
|
||||
|
||||
b.iter(|| black_box(train.to_pattern(0.0, 1.0, 100)));
|
||||
});
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("cross_correlation", size),
|
||||
size,
|
||||
|b, &size| {
|
||||
let mut train1 = SpikeTrain::new(0);
|
||||
let mut train2 = SpikeTrain::new(1);
|
||||
for i in 0..size {
|
||||
train1.record_spike(i as f64 * 0.5);
|
||||
train2.record_spike(i as f64 * 0.5 + 2.0);
|
||||
}
|
||||
|
||||
b.iter(|| black_box(train1.cross_correlation(&train2, 50.0, 1.0)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_lif_neuron,
|
||||
bench_stdp,
|
||||
bench_network,
|
||||
bench_attractor,
|
||||
bench_synchrony,
|
||||
bench_cognitive_engine,
|
||||
bench_spike_train,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
545
crates/ruvector-mincut/benches/sota_bench.rs
Normal file
545
crates/ruvector-mincut/benches/sota_bench.rs
Normal file
@@ -0,0 +1,545 @@
|
||||
//! SOTA (State-of-the-Art) Benchmarks
|
||||
//!
|
||||
//! Benchmarks for the advanced optimizations implemented from the December 2025 paper:
|
||||
//! - Cut size scaling behavior
|
||||
//! - Graph density impact
|
||||
//! - Batch operation efficiency
|
||||
//! - Memory pool performance
|
||||
//! - Lazy witness benefits
|
||||
//! - Replacement edge lookup
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use ruvector_mincut::algorithm::ReplacementEdgeIndex;
|
||||
use ruvector_mincut::instance::witness::LazyWitness;
|
||||
use ruvector_mincut::pool::BfsPool;
|
||||
use ruvector_mincut::prelude::*;
|
||||
use ruvector_mincut::wrapper::MinCutWrapper;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
// ============================================================================
|
||||
// Graph Generators
|
||||
// ============================================================================
|
||||
|
||||
/// Generate a graph with a specific minimum cut value
|
||||
/// Creates two cliques connected by k edges (min cut = k)
|
||||
fn generate_known_mincut(n_per_side: usize, mincut_value: usize, seed: u64) -> Vec<(u64, u64)> {
|
||||
let mut edges = Vec::new();
|
||||
let mut rng = StdRng::seed_from_u64(seed);
|
||||
|
||||
// First clique: vertices 0 to n_per_side-1
|
||||
for i in 0..n_per_side as u64 {
|
||||
for j in (i + 1)..n_per_side as u64 {
|
||||
edges.push((i, j));
|
||||
}
|
||||
}
|
||||
|
||||
// Second clique: vertices n_per_side to 2*n_per_side-1
|
||||
let offset = n_per_side as u64;
|
||||
for i in 0..n_per_side as u64 {
|
||||
for j in (i + 1)..n_per_side as u64 {
|
||||
edges.push((offset + i, offset + j));
|
||||
}
|
||||
}
|
||||
|
||||
// Connect with exactly mincut_value edges
|
||||
for k in 0..mincut_value {
|
||||
let u = rng.gen_range(0..n_per_side as u64);
|
||||
let v = offset + rng.gen_range(0..n_per_side as u64);
|
||||
edges.push((u, v));
|
||||
}
|
||||
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate graph with specified density (0.0 = sparse, 1.0 = complete)
|
||||
fn generate_density_graph(n: usize, density: f64, seed: u64) -> Vec<(u64, u64)> {
|
||||
let mut rng = StdRng::seed_from_u64(seed);
|
||||
let max_edges = n * (n - 1) / 2;
|
||||
let target_edges = ((max_edges as f64) * density) as usize;
|
||||
|
||||
let mut edges = Vec::with_capacity(target_edges);
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
while edges.len() < target_edges {
|
||||
let u = rng.gen_range(0..n as u64);
|
||||
let v = rng.gen_range(0..n as u64);
|
||||
if u != v {
|
||||
let key = if u < v { (u, v) } else { (v, u) };
|
||||
if seen.insert(key) {
|
||||
edges.push((u, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
edges
|
||||
}
|
||||
|
||||
/// Generate path graph
|
||||
fn generate_path(n: usize) -> Vec<(u64, u64)> {
|
||||
(0..n as u64 - 1).map(|i| (i, i + 1)).collect()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cut Size Scaling Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark how performance scales with minimum cut value
|
||||
fn bench_cut_size_scaling(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sota_cut_size_scaling");
|
||||
group.sample_size(50);
|
||||
|
||||
let n_per_side = 50; // 100 vertices total
|
||||
|
||||
// Test different min-cut values: 1, 2, 4, 8, 16, 32
|
||||
for mincut in [1, 2, 4, 8, 16, 32] {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("query_mincut", mincut),
|
||||
&mincut,
|
||||
|b, &mincut| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_known_mincut(n_per_side, mincut, 42);
|
||||
|
||||
for (u, v) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, 1.0);
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Density Impact Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark how graph density affects performance
|
||||
fn bench_density_impact(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sota_density_impact");
|
||||
group.sample_size(30);
|
||||
|
||||
let n = 200;
|
||||
|
||||
// Test different densities: 0.01, 0.05, 0.1, 0.2, 0.3
|
||||
for density_pct in [1, 5, 10, 20, 30] {
|
||||
let density = density_pct as f64 / 100.0;
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("query_density", format!("{}pct", density_pct)),
|
||||
&density,
|
||||
|b, &density| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let edges = generate_density_graph(n, density, 42);
|
||||
|
||||
for (u, v) in &edges {
|
||||
let _ = graph.insert_edge(*u, *v, 1.0);
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
wrapper
|
||||
},
|
||||
|mut wrapper| {
|
||||
let result = wrapper.query();
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Batch Operation Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark batch insert vs sequential insert
|
||||
fn bench_batch_vs_sequential(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sota_batch_operations");
|
||||
|
||||
for batch_size in [10, 50, 100, 500] {
|
||||
// Sequential inserts
|
||||
group.throughput(Throughput::Elements(batch_size as u64));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("sequential_insert", batch_size),
|
||||
&batch_size,
|
||||
|b, &batch_size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
// Base graph
|
||||
let base_edges = generate_path(500);
|
||||
for (u, v) in &base_edges {
|
||||
let _ = graph.insert_edge(*u, *v, 1.0);
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in base_edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
// New edges to insert
|
||||
let mut rng = StdRng::seed_from_u64(123);
|
||||
let new_edges: Vec<_> = (0..batch_size)
|
||||
.filter_map(|i| {
|
||||
let u = rng.gen_range(0..500);
|
||||
let v = rng.gen_range(0..500);
|
||||
if u != v && graph.insert_edge(u, v, 1.0).is_ok() {
|
||||
Some((1000 + i as u64, u, v))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(wrapper, new_edges)
|
||||
},
|
||||
|(mut wrapper, edges)| {
|
||||
for (id, u, v) in edges {
|
||||
wrapper.insert_edge(id, u, v);
|
||||
}
|
||||
black_box(wrapper)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Batch inserts
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("batch_insert", batch_size),
|
||||
&batch_size,
|
||||
|b, &batch_size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let base_edges = generate_path(500);
|
||||
for (u, v) in &base_edges {
|
||||
let _ = graph.insert_edge(*u, *v, 1.0);
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for (i, (u, v)) in base_edges.iter().enumerate() {
|
||||
wrapper.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
let mut rng = StdRng::seed_from_u64(123);
|
||||
let new_edges: Vec<_> = (0..batch_size)
|
||||
.filter_map(|i| {
|
||||
let u = rng.gen_range(0..500);
|
||||
let v = rng.gen_range(0..500);
|
||||
if u != v && graph.insert_edge(u, v, 1.0).is_ok() {
|
||||
Some((1000 + i as u64, u, v))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(wrapper, new_edges)
|
||||
},
|
||||
|(mut wrapper, edges)| {
|
||||
wrapper.batch_insert_edges(&edges);
|
||||
black_box(wrapper)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Memory Pool Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark BFS with and without pool
|
||||
fn bench_memory_pool(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sota_memory_pool");
|
||||
|
||||
for size in [100, 500, 1000, 5000] {
|
||||
// Without pool - allocate fresh each time
|
||||
group.bench_with_input(BenchmarkId::new("bfs_no_pool", size), &size, |b, &size| {
|
||||
b.iter(|| {
|
||||
let mut queue = std::collections::VecDeque::with_capacity(size);
|
||||
let mut visited = HashSet::with_capacity(size);
|
||||
let mut results = Vec::with_capacity(size);
|
||||
|
||||
// Simulate BFS work
|
||||
queue.push_back(0u64);
|
||||
visited.insert(0);
|
||||
while let Some(v) = queue.pop_front() {
|
||||
results.push(v);
|
||||
if results.len() >= size {
|
||||
break;
|
||||
}
|
||||
// Simulate adding neighbors
|
||||
for next in v + 1..v + 4 {
|
||||
if visited.insert(next) {
|
||||
queue.push_back(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
black_box(results)
|
||||
});
|
||||
});
|
||||
|
||||
// With pool - reuse allocations
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("bfs_with_pool", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
b.iter(|| {
|
||||
let mut res = BfsPool::acquire(size);
|
||||
|
||||
// Simulate BFS work
|
||||
res.queue.push_back(0);
|
||||
res.visited.insert(0);
|
||||
while let Some(v) = res.queue.pop_front() {
|
||||
res.results.push(v);
|
||||
if res.results.len() >= size {
|
||||
break;
|
||||
}
|
||||
for next in v + 1..v + 4 {
|
||||
if res.visited.insert(next) {
|
||||
res.queue.push_back(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
black_box(res.results.len())
|
||||
// Resources returned on drop
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lazy Witness Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark lazy vs eager witness materialization
|
||||
fn bench_lazy_witness(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sota_lazy_witness");
|
||||
|
||||
// Build adjacency for materialization
|
||||
let adjacency = |v: VertexId| -> Vec<VertexId> {
|
||||
// Simple linear graph for benchmarking
|
||||
if v == 0 {
|
||||
vec![1]
|
||||
} else if v < 999 {
|
||||
vec![v - 1, v + 1]
|
||||
} else {
|
||||
vec![v - 1]
|
||||
}
|
||||
};
|
||||
|
||||
// Benchmark storing witnesses without materialization
|
||||
group.bench_function("lazy_store_100", |b| {
|
||||
b.iter(|| {
|
||||
let witnesses: Vec<_> = (0..100)
|
||||
.map(|i| LazyWitness::new(i as u64, 10, i as u64))
|
||||
.collect();
|
||||
black_box(witnesses)
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark materializing just the best witness
|
||||
group.bench_function("lazy_materialize_best", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
// Create 100 lazy witnesses
|
||||
(0..100)
|
||||
.map(|i| LazyWitness::new(i as u64, 10, i as u64))
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
|witnesses| {
|
||||
// Find and materialize only the best (smallest boundary)
|
||||
let best = witnesses.iter().min_by_key(|w| w.boundary_size()).unwrap();
|
||||
let handle = best.materialize(&adjacency);
|
||||
black_box(handle)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Benchmark materializing all witnesses
|
||||
group.bench_function("eager_materialize_all", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
(0..100)
|
||||
.map(|i| LazyWitness::new(i as u64, 10, i as u64))
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
|witnesses| {
|
||||
let handles: Vec<_> = witnesses
|
||||
.iter()
|
||||
.map(|w| w.materialize(&adjacency))
|
||||
.collect();
|
||||
black_box(handles)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Replacement Edge Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark replacement edge lookup
|
||||
fn bench_replacement_edge(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sota_replacement_edge");
|
||||
|
||||
for n in [100, 500, 1000, 5000] {
|
||||
// Build tree adjacency
|
||||
let mut tree_adj: HashMap<VertexId, HashSet<VertexId>> = HashMap::new();
|
||||
for i in 0..n as u64 - 1 {
|
||||
tree_adj.entry(i).or_default().insert(i + 1);
|
||||
tree_adj.entry(i + 1).or_default().insert(i);
|
||||
}
|
||||
|
||||
// Add non-tree edges
|
||||
let mut rng = StdRng::seed_from_u64(42);
|
||||
let mut idx = ReplacementEdgeIndex::new(n);
|
||||
|
||||
// Add tree edges
|
||||
for i in 0..n as u64 - 1 {
|
||||
idx.add_tree_edge(i, i + 1);
|
||||
}
|
||||
|
||||
// Add non-tree edges (skip connections)
|
||||
for _ in 0..n / 5 {
|
||||
let u = rng.gen_range(0..n as u64);
|
||||
let v = rng.gen_range(0..n as u64);
|
||||
if u != v && (u as i64 - v as i64).abs() > 1 {
|
||||
idx.add_non_tree_edge(u, v);
|
||||
}
|
||||
}
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("find_replacement", n), &n, |b, _| {
|
||||
b.iter_batched(
|
||||
|| idx.clone(),
|
||||
|mut idx| {
|
||||
// Find replacement for middle edge
|
||||
let u = (n / 2) as u64;
|
||||
let v = u + 1;
|
||||
let result = idx.find_replacement(u, v, &tree_adj);
|
||||
black_box(result)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Binary Search Instance Lookup Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark binary search vs linear instance lookup
|
||||
fn bench_instance_lookup(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sota_instance_lookup");
|
||||
|
||||
for num_instances in [10, 50, 100, 500] {
|
||||
// Simulate instance bounds
|
||||
let instances: Vec<(u64, u64)> = (0..num_instances)
|
||||
.map(|i| {
|
||||
let min = ((1.2f64).powi(i as i32)).floor() as u64;
|
||||
let max = ((1.2f64).powi((i + 1) as i32)).floor() as u64;
|
||||
(min.max(1), max.max(1))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Linear search
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("linear_search", num_instances),
|
||||
&num_instances,
|
||||
|b, _| {
|
||||
b.iter(|| {
|
||||
let target = 50u64;
|
||||
let found = instances
|
||||
.iter()
|
||||
.position(|(min, max)| target >= *min && target <= *max);
|
||||
black_box(found)
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Binary search
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("binary_search", num_instances),
|
||||
&num_instances,
|
||||
|b, _| {
|
||||
b.iter(|| {
|
||||
let target = 50u64;
|
||||
let found = instances.binary_search_by(|(min, max)| {
|
||||
if target < *min {
|
||||
std::cmp::Ordering::Greater
|
||||
} else if target > *max {
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
});
|
||||
black_box(found)
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Criterion Groups
|
||||
// ============================================================================
|
||||
|
||||
criterion_group!(cut_scaling, bench_cut_size_scaling,);
|
||||
|
||||
criterion_group!(density, bench_density_impact,);
|
||||
|
||||
criterion_group!(batch_ops, bench_batch_vs_sequential,);
|
||||
|
||||
criterion_group!(memory, bench_memory_pool, bench_lazy_witness,);
|
||||
|
||||
criterion_group!(lookup, bench_replacement_edge, bench_instance_lookup,);
|
||||
|
||||
criterion_main!(cut_scaling, density, batch_ops, memory, lookup);
|
||||
Reference in New Issue
Block a user