A Rust implementation of the SprayList data structure - a scalable concurrent priority queue with relaxed ordering semantics.
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.
- 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
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.
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
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
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
cargo add spragga
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);
}
}
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());
}
Creates a new empty SprayList with default parameters.
Creates a new SprayList with custom spray parameters.
Inserts a key-value pair. Returns true
if successful, false
if key already exists.
Removes and returns an element from near the minimum of the list. Due to the relaxed semantics, this may not be the exact minimum.
Returns the number of elements in the list.
Returns true if the list is empty.
Returns the minimum key without removing it.
Sets the expected number of threads for parameter tuning.
The SprayList is built on top of a lock-free skip list and uses a "spray" operation for the DeleteMin functionality:
-
Random Walk: Instead of always removing the minimum element, DeleteMin performs a random walk starting from the head of the list.
-
Spray Width: The maximum distance of the random walk is O(p log³ p) where p is the number of threads.
-
Collision Avoidance: Multiple threads performing DeleteMin are likely to delete different elements, reducing contention.
- 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.
This implementation includes comprehensive benchmarks to measure SprayList performance and compare with the original research.
Run all benchmarks:
./run_throughput.sh
Generate CSV data for analysis:
./run_throughput_csv.sh
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
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)
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
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
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 |
- ops/sec: Operations per second (higher is better)
- Success Rate: Percentage of successful operations
- Insert/Delete/Peek counts: Breakdown of operation types
- 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
- 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
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
To analyze benchmark results:
- Criterion HTML Reports: Available in
target/criterion/*/report/index.html
- CSV Data: Use
--csv
flag for data processing with external tools - Raw Data: Located in
target/criterion/
directory
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
- 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.
- 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
This project is licensed under the MIT License.