Scalable Delayed Dealloc
A scalable lock-free delayed memory reclaimer that emulates garbage collection by tracking memory reachability.
The delayed deallocation algorithm is based on a variant of epoch-based reclamation where retired memory chunks are temporarily kept in thread-local storage until they are no longer reachable. It is similar to crossbeam_epoch, however, users will find sdd more straightforward to use as it provides both smart pointer and raw pointer types. For instance, sdd::AtomicOwned, sdd::Owned, sdd::AtomicShared, and sdd::Shared automatically retire the contained value when the last reference is dropped.
Features
- Lock-free epoch-based reclamation. [^1]
- Provides both managed and unmanaged pointer types.
Loomsupport:features = ["loom"].
Examples
AtomicOwnedandOwnedfor managed instances.AtomicSharedandSharedfor reference-counted instances: slower thanAtomicOwnedandAtomicRaw.AtomicRawfor manual pointer operations: faster thanAtomicOwnedandAtomicShared.
use ;
use Relaxed;
// `AtomicOwned<T>` and `Owned<T>` are smart pointers that exclusively own an instance of type `T`.
//
// `atomic_owned` owns `19`.
let atomic_owned: = new;
// `AtomicShared<T>` and `Shared<T>` are smart pointers that retain shared ownership of an instance
// of type `T`.
//
// These types use an atomic counter to track references, which makes them generally slower than
// `AtomicOwned<T>` and `AtomicRaw<T>`.
//
// `atomic_shared` holds a reference to `17`.
let atomic_shared: = new;
// `AtomicRaw<T>` is analogous to `AtomicPtr<T>` in the standard library.
//
// `atomic_raw` can be used for atomic raw pointer operations.
let atomic_raw: = null;
// `guard` prevents the garbage collector from dropping reachable instances.
let guard = new;
// `ptr` is protected by `guard` and cannot outlive it.
let mut ptr: = atomic_owned.load;
assert_eq!;
// `atomic_owned`, `atomic_shared`, and `atomic_raw` can be tagged.
atomic_owned.update_tag_if;
atomic_shared.update_tag_if;
atomic_raw.compare_exchange;
// `ptr` is not tagged, so the compare-and-swap operation fails.
assert!;
// `ptr` can be tagged.
ptr.set_tag;
// The ownership of the contained instance is transferred to the return value.
let prev = atomic_shared.swap.0.unwrap;
assert_eq!;
// `17` will be garbage-collected later.
drop;
// `atomic_shared` can be converted into a `Shared`.
let shared: = atomic_shared.into_shared.unwrap;
assert_eq!;
// `Owned` can be converted into a `RawPtr` and set to `atomic_raw`.
let owned = new;
let raw_ptr = owned.into_raw;
atomic_raw.store;
// `18` and `19` will be garbage-collected later.
drop;
drop;
// `19` is still valid as `guard` prevents the garbage collector from dropping it.
assert_eq!;
// `7` can also be accessed through `atomic_raw`.
let raw_ptr = atomic_raw.load;
// Using a `RawPtr` requires an `unsafe` block.
assert_eq!;
// `RawPtr` can be converted into an `Owned` for automatic memory management.
let owned = unsafe ;
drop;
// Execution of a closure can be deferred until all current readers are gone.
guard.defer_execute;
drop;
// `Owned` and `Shared` can be nested.
let shared_nested: = new;
assert_eq!;
// `PrivateCollector` is useful when memory must be reclaimed by a strict deadline.
let private_collector = new;
let owned = new;
let shared = new;
unsafe
// Dropping `private_collector` immediately deallocates both `37` and `43`.
drop;
Memory Overhead
Retired instances are stored in intrusive queues in thread-local storage, requiring additional space for (AtomicPtr<()> /* next */, fn(*mut ()) /* drop */) per instance.
Performance
The average time taken to enter and exit a protected region: less than a nanosecond on Apple M4 Pro.
Applications
sdd provides widely used lock-free concurrent data structures, including LinkedList, Bag, Queue, and Stack. Note that these are implemented using AtomicShared rather than AtomicRaw to prioritize functionality over performance. For performance-critical applications, consider implementing data structures with AtomicRaw instead.
LinkedList
LinkedList is a trait that implements lock-free concurrent singly linked list operations and provides a method for marking a linked list entry with a user-defined state.
use Relaxed;
use ;
;
let guard = new;
let head: L = default;
let tail: = new;
// A new entry is pushed.
assert!;
assert!;
// Users can mark a flag on an entry.
head.mark;
assert!;
// `next_ptr` traverses to the next entry in the linked list.
let next_ptr = head.next_ptr;
assert_eq!;
// Once `tail` is deleted, it becomes unreachable.
tail.delete_self;
assert!;
Bag
Bag is a concurrent lock-free unordered container that is completely opaque, disallowing access to contained instances until they are popped. It is especially efficient when the number of contained instances is maintained under ARRAY_LEN (default: usize::BITS / 2).
use Bag;
let bag: = default;
bag.push;
assert!;
assert_eq!;
assert!;
Queue
Queue is a concurrent lock-free first-in-first-out container.
use Queue;
let queue: = default;
queue.push;
assert!;
assert!;
assert_eq!;
assert_eq!;
assert!;
Stack
Stack is a concurrent lock-free last-in-first-out container.
use Stack;
let stack: = default;
stack.push;
stack.push;
assert_eq!;
assert_eq!;
assert!;
Changelog
[^1]: PrivateCollector is not lock-free, though the lock is not contended.