Expand description
A thread-safe, compressed vector of integers with fixed-width encoding.
This module provides AtomicFixedVec, a data structure that behaves like
FixedVec but allows for concurrent access and
modification from multiple threads. It is designed for scenarios where a
large collection of integers must be shared and mutated in a parallel
environment.
All operations that modify the vector’s contents are implemented using atomic instructions (e.g., compare-and-swap loops), ensuring thread safety without requiring a global lock.
§Atomicity Guarantees and Locking
The atomicity of operations depends on the configured bit_width.
-
Power-of-Two
bit_width: When thebit_widthis a power of two (e.g., 2, 4, 8, 16, 32), and it evenly divides the 64-bit word size, most operations can be performed with lock-free atomic instructions. This is because each element is guaranteed to be fully contained within a singleAtomicU64word. -
Non-Power-of-Two
bit_width: When thebit_widthis not a power of two, an element’s value may span across the boundary of twoAtomicU64words. Modifying such an element requires updating two words simultaneously, which cannot be done in a single atomic hardware instruction.
To handle this case, AtomicFixedVec uses a technique called lock striping.
It maintains a pool of parking_lot::Mutex locks. When an operation needs
to modify a value that spans two words, it acquires a lock for that specific
memory region. This ensures that the two-word update is itself atomic with
respect to other threads, while still allowing concurrent operations on
different parts of the vector. This approach avoids a single global lock,
preserving a high degree of parallelism.
Future version may introduce a more sophisticated locking strategy
§Performance Considerations
The trade-off is between memory compactness and performance. While a
non-power-of-two bit_width provides the most space-efficient storage,
it may incur a performance overhead for write operations that span word
boundaries due to locking.
For write-heavy, performance-critical workloads, choosing a power-of-two
bit_width (e.g., by using BitWidth::PowerOfTwo) is recommended to
ensure all operations remain lock-free.
§Examples
§Basic Usage
use compressed_intvec::prelude::*;
use compressed_intvec::fixed::{AtomicFixedVec, UAtomicFixedVec};
use std::sync::Arc;
use std::thread;
use std::sync::atomic::Ordering;
// Create from a slice using the builder.
let initial_data: Vec<u32> = vec![10, 20, 30, 40];
let atomic_vec: Arc<UAtomicFixedVec<u32>> = Arc::new(
AtomicFixedVec::builder()
.build(&initial_data)
.unwrap()
);
// Share the vector across threads.
let mut handles = vec![];
for i in 0..4 {
let vec_clone = Arc::clone(&atomic_vec);
handles.push(thread::spawn(move || {
// Each thread atomically updates its own slot.
vec_clone.store(i, 63, Ordering::SeqCst);
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(atomic_vec.load(3, Ordering::SeqCst), 63);§Storing Signed Integers
AtomicFixedVec can also store signed integers. The underlying Storable
trait uses zig-zag encoding to store signed values efficiently, so that
small negative numbers require few bits, just like small positive numbers.
use compressed_intvec::prelude::*;
use compressed_intvec::fixed::{AtomicFixedVec, SAtomicFixedVec};
use std::sync::Arc;
use std::sync::atomic::Ordering;
// The values range from -2 to 1. To also store -3 later, we need 3 bits.
let initial_data: Vec<i16> = vec![-2, -1, 0, 1];
let atomic_vec: Arc<SAtomicFixedVec<i16>> = Arc::new(
AtomicFixedVec::builder()
.bit_width(BitWidth::Explicit(3)) // Explicitly set bit width
.build(&initial_data)
.unwrap()
);
assert_eq!(atomic_vec.bit_width(), 3);
assert_eq!(atomic_vec.load(0, Ordering::SeqCst), -2);
// Atomically update a value.
atomic_vec.store(0, -3, Ordering::SeqCst);
assert_eq!(atomic_vec.load(0, Ordering::SeqCst), -3);//! ## Parallel Iteration
When the parallel feature is enabled (by default is enabled), you can use par_iter to process
the vector’s elements concurrently.
use compressed_intvec::prelude::*;
use compressed_intvec::fixed::{AtomicFixedVec, UAtomicFixedVec};
use rayon::prelude::*;
let data: Vec<u32> = (0..10_000).collect();
let vec: UAtomicFixedVec<u32> = AtomicFixedVec::builder()
.build(&data)
.unwrap();
// Use the parallel iterator to find the sum of all even numbers.
let sum_of_evens: u32 = vec.par_iter()
.filter(|&x| x % 2 == 0)
.sum();
let expected_sum: u32 = (0..10_000).filter(|&x| x % 2 == 0).sum();
assert_eq!(sum_of_evens, expected_sum);§Memory Ordering and Locking
The memory Ordering specified in methods like load, store, or
fetch_add is always respected, but its interaction with the internal locking
mechanism is important to understand.
-
Lock-Free Path: When an element is fully contained within a single
u64word, the specifiedOrderingis applied directly to the underlying atomic instructions, providing the standard guarantees described in the Rust documentation. -
Locked Path: When an element spans two
u64words, a fine-grained mutex is acquired. This lock ensures that the two-word operation is atomic with respect to other locked operations on the same memory region. The specifiedOrderingis then applied to the atomic writes performed within the locked critical section. This guarantees that the effects of the operation become visible to other threads according to the chosen ordering, but the visibility is still mediated by the mutual exclusion provided by the lock.
Modules§
- builder
AtomicFixedVecBuilder- macros
- Macros for
AtomicFixedVec
Structs§
- Atomic
Fixed Vec - A thread-safe, compressed, randomly accessible vector of integers with
fixed-width encoding, backed by
u64atomic words. - Atomic
Fixed VecIter - An iterator over the elements of a borrowed
AtomicFixedVec. - Atomic
MutProxy - A proxy object for mutable access to an element within an
AtomicFixedVecduring parallel iteration.
Type Aliases§
- SAtomic
Fixed Vec - A thread-safe
FixedVecfor signed integers. - UAtomic
Fixed Vec - A thread-safe
FixedVecfor unsigned integers.