Skip to content

maschad/spragga

Repository files navigation

Spragga

A Rust implementation of the SprayList data structure - a scalable concurrent priority queue with relaxed ordering semantics.

Overview

SprayList is a concurrent data structure designed to address the scalability bottleneck in traditional priority queues. Based on the paper "The SprayList: A Scalable Relaxed Priority Queue" by Alistarh, Kopinsky and Li, it provides high-performance concurrent access at the cost of relaxed ordering guarantees.

Key Features

  • Scalable: Avoids sequential bottlenecks through "spraying" technique
  • Lock-free: Built on top of a non-blocking skip list
  • Relaxed ordering: DeleteMin returns an element among the first O(p log³ p) elements
  • Thread-safe: Supports concurrent insert and delete operations

High-Concurrency Priority Queue Applications

SprayList is particularly well-suited for applications that need high-throughput priority queue operations under concurrent access patterns. Traditional priority queues become bottlenecks in multi-threaded environments, but SprayList's relaxed ordering enables exceptional scalability.

When to Use SprayList

Ideal Use Cases:

  • Task scheduling systems where approximate priority ordering is acceptable
  • Event simulation engines processing thousands of concurrent events
  • Load balancing systems distributing work across multiple processors
  • Real-time systems where throughput matters more than strict ordering
  • Gaming engines managing entity priorities and update schedules
  • Network packet processing with quality-of-service requirements

Performance Characteristics:

  • Single-threaded performance: 42+ million operations/sec
  • Multi-threaded scaling: Maintains 100K+ ops/sec even at 16 threads
  • Contention resistance: Performance degrades gracefully under high load
  • Memory efficient: O(n) space complexity with minimal overhead

Trade-offs

SprayList trades strict priority ordering for massive scalability improvements. The relaxed semantics mean that delete_min() returns an element from the "spray range" (among the first O(p log³ p) elements) rather than the exact minimum. This trade-off enables:

  • 10-100x better throughput compared to traditional concurrent priority queues
  • Reduced contention through randomized element selection
  • Better cache locality by avoiding hot spots at the queue head

Alternative Considerations

Use traditional priority queues when:

  • Exact ordering is critical to correctness
  • Single-threaded or low-concurrency access patterns
  • Small queue sizes where contention is not an issue

Usage

cargo add spragga

Basic Example

use spragga::SprayList;

fn main() {
    // Create a new SprayList
    let spray = SprayList::new();

    // Insert elements
    spray.insert(&5, &"five");
    spray.insert(&3, &"three");
    spray.insert(&7, &"seven");
    spray.insert(&1, &"one");

    // Delete minimum (may not be the exact minimum due to relaxed semantics)
    while let Some((key, value)) = spray.delete_min() {
        println!("Removed: {} -> {}", key, value);
    }
}

Concurrent Example

use spragga::SprayList;
use std::sync::Arc;
use std::thread;

fn main() {
    let spray = Arc::new(SprayList::new());
    spray.set_num_threads(4); // Optimize for 4 threads

    let mut handles = vec![];

    // Spawn threads to insert values
    for i in 0..4 {
        let spray_clone = spray.clone();
        let handle = thread::spawn(move || {
            for j in 0..100 {
                spray_clone.insert(i * 100 + j, format!("value-{}", j));
            }
        });
        handles.push(handle);
    }

    // Wait for all threads
    for handle in handles {
        handle.join().unwrap();
    }

    println!("Inserted {} elements", spray.len());
}

API

SprayList::new() -> Self

Creates a new empty SprayList with default parameters.

SprayList::with_params(params: SprayParams) -> Self

Creates a new SprayList with custom spray parameters.

insert(&self, key: K, value: V) -> bool

Inserts a key-value pair. Returns true if successful, false if key already exists.

delete_min(&self) -> Option<(K, V)>

Removes and returns an element from near the minimum of the list. Due to the relaxed semantics, this may not be the exact minimum.

len(&self) -> usize

Returns the number of elements in the list.

is_empty(&self) -> bool

Returns true if the list is empty.

peek_min(&self) -> Option<K>

Returns the minimum key without removing it.

set_num_threads(&self, num_threads: usize)

Sets the expected number of threads for parameter tuning.

Implementation Details

The SprayList is built on top of a lock-free skip list and uses a "spray" operation for the DeleteMin functionality:

  1. Random Walk: Instead of always removing the minimum element, DeleteMin performs a random walk starting from the head of the list.

  2. Spray Width: The maximum distance of the random walk is O(p log³ p) where p is the number of threads.

  3. Collision Avoidance: Multiple threads performing DeleteMin are likely to delete different elements, reducing contention.

Performance Characteristics

  • Insert: O(log n) expected time
  • DeleteMin: O(log³ p) expected time, independent of list size
  • Space: O(n) expected space

The relaxed ordering allows the data structure to scale much better than traditional priority queues under high contention.

Benchmarking

This implementation includes comprehensive benchmarks to measure SprayList performance and compare with the original research.

Quick Start

Run all benchmarks:

./run_throughput.sh

Generate CSV data for analysis:

./run_throughput_csv.sh

Benchmark Types

1. Throughput Scaling Tests

Tests performance with different thread counts (1, 2, 4, 8, 16 threads):

cargo bench --bench spraylist_bench -- bench_throughput_scaling

Sample Output:

Threads: 1, Throughput: 42,169,478 ops/sec
Threads: 2, Throughput: 120,098 ops/sec
Threads: 4, Throughput: 139,785 ops/sec
Threads: 8, Throughput: 85,580 ops/sec
Threads: 16, Throughput: 66,026 ops/sec

2. Mixed Workload Tests

Tests different ratios of insert/delete vs. read operations:

cargo bench --bench spraylist_bench -- bench_mixed_workloads

Tests update percentages: 0% (read-only), 25%, 50%, 75%, 100% (write-only)

3. Parameter Comparison Tests

Compares different SprayList configurations:

cargo bench --bench spraylist_bench -- bench_spray_parameters

Tests configurations:

  • default: Standard parameters (spray_base=32, spray_height=20)
  • small_spray: Reduced spray width (spray_base=16, spray_height=10)
  • large_spray: Increased spray width (spray_base=64, spray_height=30)
  • no_fallback: Disabled exact deletemin fallback

Custom Throughput Testing

Use the standalone throughput test binary for custom configurations:

# Build the test binary
cargo build --release --bin throughput_test

# Basic usage
./target/release/throughput_test --threads 8 --duration 10 --update-pct 100

# Scaling test with CSV output
./target/release/throughput_test --scaling --csv

# Show all options
./target/release/throughput_test --help

Command Line Options

Option Description Default
--threads <N> Number of threads 4
--duration <N> Test duration in seconds 5
--update-pct <N> Update percentage (0-100) 100
--initial-size <N> Initial data structure size 1000
--total-ops <N> Target total operations 1,000,000
--csv Output in CSV format false
--scaling Run thread scaling test false

Understanding Results

Throughput Metrics

  • ops/sec: Operations per second (higher is better)
  • Success Rate: Percentage of successful operations
  • Insert/Delete/Peek counts: Breakdown of operation types

Expected Performance Characteristics

  • Single Thread: Baseline performance, limited by sequential execution
  • Low Thread Count (2-4): Near-linear scaling due to reduced contention
  • High Thread Count (8+): Sublinear scaling as contention increases, but still maintains good throughput due to spray mechanism

Interpreting Success Rates

  • 100% Success: All operations completed successfully
  • 95-99% Success: Normal for high-contention scenarios, indicates effective collision avoidance
  • <90% Success: May indicate excessive contention or configuration issues

Comparison with Reference Implementation

The benchmarks are designed to match the methodology from the original SprayList research:

  • Similar thread counts (1, 2, 4, 8, 16)
  • Comparable operation mixes and durations
  • CSV output format for data analysis
  • Performance metrics aligned with the paper's evaluation

Benchmark Results Analysis

To analyze benchmark results:

  1. Criterion HTML Reports: Available in target/criterion/*/report/index.html
  2. CSV Data: Use --csv flag for data processing with external tools
  3. Raw Data: Located in target/criterion/ directory

Testing

The project includes comprehensive tests to verify correctness and thread safety:

# Run all tests
cargo test

# Run specific test suites
cargo test test_concurrent_operations
cargo test test_thread_safety
cargo test test_spray_parameters

Test Categories

  • Sequential Tests: Basic functionality with single-threaded operations
  • Concurrent Tests: Multi-threaded correctness validation
  • Parameter Tests: Custom SprayList configuration validation
  • Thread Safety Tests: High-contention stress testing with multiple threads
  • Fallback Tests: Validation of exact deletemin fallback behavior

The test suite includes stress tests with up to 800 concurrent operations across 8 threads to ensure robust behavior under high contention.

Limitations

  • Elements returned by DeleteMin may not be in strict ascending order
  • The ordering relaxation is bounded by O(p log³ p) where p is the number of threads
  • Best suited for applications that can tolerate approximate priority ordering

References

License

This project is licensed under the MIT License.

About

A Spraylist implementation in Rust

Resources

License

Stars

Watchers

Forks

Packages

No packages published