acmap 0.1.0

An actor-style sharded async map inspired by DashMap-like APIs.
Documentation
use acmap::AcMap;
use dashmap::DashMap;
use std::{
    collections::HashMap,
    sync::Arc,
    time::{Duration, Instant},
};
use tokio::sync::Mutex;

const NUM_ITERATIONS: u64 = 1_000_000;
const NUM_WORKERS: u64 = 8;
const BATCH_SIZE: usize = 512;

#[derive(Debug, Clone)]
struct BenchResult {
    name: &'static str,
    elapsed: Duration,
    ops: u64,
}

#[tokio::main]
async fn main() {
    println!(
        "Benchmark config: ops={}, workers={}",
        NUM_ITERATIONS, NUM_WORKERS
    );

    let single_thread = vec![
        run_sync("HashMap", NUM_ITERATIONS, test_hashmap),
        run_sync("DashMap", NUM_ITERATIONS, test_dashmap),
        run_async("AcMap", NUM_ITERATIONS, test_amap).await,
        run_async("AcMap(Fast)", NUM_ITERATIONS, test_amap_fast).await,
        run_async("AcMap(FastBatch)", NUM_ITERATIONS, test_amap_fast_batch).await,
    ];

    let multi_thread = vec![
        run_async(
            "HashMap+Mutex(MT)",
            NUM_ITERATIONS,
            test_hashmap_multi_thread,
        )
        .await,
        run_async("DashMap(MT)", NUM_ITERATIONS, test_dashmap_multi_thread).await,
        run_async("AcMap(MT)", NUM_ITERATIONS, test_amap_multi_thread).await,
        run_async(
            "AcMap(Fast,MT)",
            NUM_ITERATIONS,
            test_amap_fast_multi_thread,
        )
        .await,
        run_async(
            "AcMap(FastBatch,MT)",
            NUM_ITERATIONS,
            test_amap_fast_batch_multi_thread,
        )
        .await,
    ];

    print_section("Single-thread", &single_thread);
    print_section("Multi-thread", &multi_thread);
}

fn run_sync(name: &'static str, expected_len: u64, f: fn() -> usize) -> BenchResult {
    let start = Instant::now();
    let len = f();
    let elapsed = start.elapsed();

    assert_eq!(len as u64, expected_len, "{name} length mismatch");

    BenchResult {
        name,
        elapsed,
        ops: expected_len,
    }
}

async fn run_async<Fut>(name: &'static str, expected_len: u64, f: fn() -> Fut) -> BenchResult
where
    Fut: std::future::Future<Output = usize>,
{
    let start = Instant::now();
    let len = f().await;
    let elapsed = start.elapsed();

    assert_eq!(len as u64, expected_len, "{name} length mismatch");

    BenchResult {
        name,
        elapsed,
        ops: expected_len,
    }
}

fn print_section(title: &str, results: &[BenchResult]) {
    println!("\n=== {title} ===");
    println!(
        "{:<18} {:>12} {:>14} {:>12}",
        "Case", "Elapsed", "Throughput", "Speedup"
    );

    let baseline = results[0].elapsed.as_secs_f64();
    for r in results {
        let secs = r.elapsed.as_secs_f64();
        let mops = (r.ops as f64 / secs) / 1_000_000.0;
        let speedup = baseline / secs;

        println!(
            "{:<18} {:>10.3}s {:>11.3} Mops/s {:>9.2}x",
            r.name, secs, mops, speedup
        );
    }
}

fn test_hashmap() -> usize {
    let mut map = HashMap::new();

    for i in 0..NUM_ITERATIONS {
        map.insert(i, i);
    }

    map.len()
}

fn test_dashmap() -> usize {
    let map = DashMap::new();

    for i in 0..NUM_ITERATIONS {
        map.insert(i, i);
    }

    map.len()
}

async fn test_amap() -> usize {
    let map = AcMap::<u64, u64>::new();

    for i in 0..NUM_ITERATIONS {
        map.insert(i, i).await;
    }

    map.len().await
}

async fn test_amap_fast() -> usize {
    let map = AcMap::<u64, u64>::new();

    for i in 0..NUM_ITERATIONS {
        map.insert_fast(i, i);
    }

    map.len().await
}

async fn test_amap_fast_batch() -> usize {
    let map = AcMap::<u64, u64>::new();
    for start in (0..NUM_ITERATIONS).step_by(BATCH_SIZE) {
        let end = (start + BATCH_SIZE as u64).min(NUM_ITERATIONS);
        map.insert_fast_batch((start..end).map(|i| (i, i)));
    }
    map.len().await
}

fn worker_range(worker: u64) -> (u64, u64) {
    let chunk = NUM_ITERATIONS / NUM_WORKERS;
    let start = worker * chunk;
    let end = if worker == NUM_WORKERS - 1 {
        NUM_ITERATIONS
    } else {
        start + chunk
    };
    (start, end)
}

async fn test_hashmap_multi_thread() -> usize {
    let map = Arc::new(Mutex::new(HashMap::new()));
    let mut handles = Vec::new();

    for worker in 0..NUM_WORKERS {
        let map = Arc::clone(&map);
        let (start, end) = worker_range(worker);
        handles.push(tokio::spawn(async move {
            for i in start..end {
                map.lock().await.insert(i, i);
            }
        }));
    }

    for handle in handles {
        handle.await.expect("hashmap worker panicked");
    }

    map.lock().await.len()
}

async fn test_dashmap_multi_thread() -> usize {
    let map = Arc::new(DashMap::new());
    let mut handles = Vec::new();

    for worker in 0..NUM_WORKERS {
        let map = Arc::clone(&map);
        let (start, end) = worker_range(worker);
        handles.push(tokio::spawn(async move {
            for i in start..end {
                map.insert(i, i);
            }
        }));
    }

    for handle in handles {
        handle.await.expect("dashmap worker panicked");
    }

    map.len()
}

async fn test_amap_multi_thread() -> usize {
    let map = AcMap::<u64, u64>::new();
    let mut handles = Vec::new();

    for worker in 0..NUM_WORKERS {
        let map = map.clone();
        let (start, end) = worker_range(worker);
        handles.push(tokio::spawn(async move {
            for i in start..end {
                map.insert(i, i).await;
            }
        }));
    }

    for handle in handles {
        handle.await.expect("amap worker panicked");
    }

    map.len().await
}

async fn test_amap_fast_multi_thread() -> usize {
    let map = AcMap::<u64, u64>::new();
    let mut handles = Vec::new();

    for worker in 0..NUM_WORKERS {
        let map = map.clone();
        let (start, end) = worker_range(worker);
        handles.push(tokio::spawn(async move {
            for i in start..end {
                map.insert_fast(i, i);
            }
        }));
    }

    for handle in handles {
        handle.await.expect("amap fast worker panicked");
    }

    map.len().await
}

async fn test_amap_fast_batch_multi_thread() -> usize {
    let map = AcMap::<u64, u64>::new();
    let mut handles = Vec::new();

    for worker in 0..NUM_WORKERS {
        let map = map.clone();
        let (start, end) = worker_range(worker);
        handles.push(tokio::spawn(async move {
            for chunk_start in (start..end).step_by(BATCH_SIZE) {
                let chunk_end = (chunk_start + BATCH_SIZE as u64).min(end);
                map.insert_fast_batch((chunk_start..chunk_end).map(|i| (i, i)));
            }
        }));
    }

    for handle in handles {
        handle.await.expect("amap fast batch worker panicked");
    }

    map.len().await
}