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_width
is 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 singleAtomicU64
word. -
Non-Power-of-Two
bit_width
: When thebit_width
is not a power of two, an element’s value may span across the boundary of twoAtomicU64
words. 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
u64
word, the specifiedOrdering
is applied directly to the underlying atomic instructions, providing the standard guarantees described in the Rust documentation. -
Locked Path: When an element spans two
u64
words, 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 specifiedOrdering
is 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
AtomicFixedVec
Builder- macros
- Macros for
AtomicFixedVec
Structs§
- Atomic
Fixed Vec - A thread-safe, compressed, randomly accessible vector of integers with
fixed-width encoding, backed by
u64
atomic 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
AtomicFixedVec
during parallel iteration.
Type Aliases§
- SAtomic
Fixed Vec - A thread-safe
FixedVec
for signed integers. - UAtomic
Fixed Vec - A thread-safe
FixedVec
for unsigned integers.