| Crates.io | spragga |
| lib.rs | spragga |
| version | 1.0.0 |
| created_at | 2025-06-25 15:32:21.289312+00 |
| updated_at | 2025-06-25 15:32:21.289312+00 |
| description | A scalable concurrent priority queue with relaxed ordering semantics |
| homepage | |
| repository | https://github.com/maschad/spragga |
| max_upload_size | |
| id | 1726035 |
| size | 113,258 |
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.
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:
Performance Characteristics:
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:
Use traditional priority queues when:
Add this to your Cargo.toml:
[dependencies]
spragga = "1.0.0"
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());
}
SprayList::new() -> SelfCreates a new empty SprayList with default parameters.
SprayList::with_params(params: SprayParams) -> SelfCreates a new SprayList with custom spray parameters.
insert(&self, key: K, value: V) -> boolInserts 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) -> usizeReturns the number of elements in the list.
is_empty(&self) -> boolReturns 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.
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.
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:
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 |
The benchmarks are designed to match the methodology from the original SprayList research:
To analyze benchmark results:
target/criterion/*/report/index.html--csv flag for data processing with external toolstarget/criterion/ directoryThe 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
The test suite includes stress tests with up to 800 concurrent operations across 8 threads to ensure robust behavior under high contention.
This project is licensed under the MIT License.