Expand description
§RingAl - A High-Performance Ring Allocator
RingAl is a blazing-fast ring allocator optimized for short-lived buffer allocations. It uses a circular allocation pattern, which grants low-cost allocations as long as allocations are ephemeral. Long-lived allocations can degrade performance by clogging the allocator.
§Key Features
- Preallocation Strategy: Establish a fixed-size backing store of
N
bytes. - Efficient Small Buffer Management: Allocate smaller buffers efficiently within the preallocated store.
- Thread-Safe Operations: Buffers can be safely shared across threads, offering clone optimizations similar to
Arc
. - Timely Buffer Deallocation: Buffers should be dropped before the allocator wraps around to reuse the same memory, ensuring optimal recycling.
RingAl focuses on efficient memory management with a dynamic memory pool that evolves its backing store using something called guard sequences. Guard sequences are usually exclusively owned by allocator when they are not locked (i.e. not guarding the allocation), and when they do become locked, the memory region (single usize) backing the guard sequence, is only accessed using Atomic operations. This allows for one thread (the owner of buffer) to release the guard (write to guard sequence), when the buffer is no longer in use and the other thread, where allocator is running, to perform synchronized check of whether guard sequence have been released.
The design allows for safe multi-threaded access: one thread can write while one other can
read. If a buffer is still occupied after allocator wraps around, the allocation attempt just
returns None
, prompting a retry.
Note: RingAl itself isn’t Sync
, so it cannot be accessed from multiple threads concurrently.
Instead, using thread-local storage is encouraged.
§Allocation Strategies
- Exact Fit: Perfectly matches the requested and available sizes.
- Oversized Guards: Splits larger regions to fit the requested size, potentially causing fragmentation.
- Undersized Guards: Merges smaller regions to accommodate requests, aiding in defragmentation.
- Capacity Constraints: Fails if the backing store cannot satisfy the request, signaling with
None
.
§Optional Features
tls
(Thread-Local Storage): Enables allocation requests without explicitly passing the allocator. It usesRefCell
for thread-local data, offering convenience at a minor performance cost.drop
(Early Deallocation): Allows the allocator to deallocate its resources upon request. If activated, it employs aDrop
implementation that ensures no memory leaks by blocking until all buffers are released.
§Usage Examples
§Extendable Buffer
let mut allocator = RingAl::new(1024);
let mut buffer = allocator.extendable(64).unwrap();
let msg = b"hello world, this message is longer than allocated capacity...";
let size = buffer.write(msg).unwrap();
let fixed = buffer.finalize();
assert_eq!(fixed.as_ref(), msg);
assert_eq!(fixed.len(), size);
§Fixed Buffer
let mut allocator = RingAl::new(1024);
let mut buffer = allocator.fixed(256).unwrap();
let size = buffer.write(b"hello world, short message").unwrap();
assert_eq!(buffer.len(), size);
assert!(buffer.spare() >= 256 - size);
§Multi-threaded Environment
let mut allocator = RingAl::new(1024);
let (tx, rx) = channel();
let mut buffer = allocator.fixed(64).unwrap();
let _ = buffer.write(b"message to another thread").unwrap();
let handle = std::thread::spawn(move || {
let buffer: FixedBufMut = rx.recv().unwrap();
let readonly = buffer.freeze();
for _ in 0..16 {
let msg: FixedBuf = readonly.clone();
let msg_str = std::str::from_utf8(&msg[..]).unwrap();
println!("{msg_str}");
}
});
tx.send(buffer);
handle.join().unwrap();
§Generic buffers
struct MyType {
field1: usize,
field2: u128
}
let mut buffer = allocator.generic::<MyType>(16).unwrap();
buffer.push(MyType { field1: 42, field2: 43 });
assert_eq!(buffer.len(), 1);
let t = buffer.pop().unwrap();
assert_eq!(t.field1, 42);
assert_eq!(t.field2, 43);
§Thread Local Storage
// init the thread local allocator, should be called just once, any thread
// spawned afterwards, will have access to their own instance of the allocator
ringal!(@init, 1024);
// allocate fixed buffer
let mut fixed = ringal!(@fixed, 64).unwrap();
let _ = fixed.write(b"hello world!").unwrap();
// allocate extendable buffer, and pass a callback to operate on it
// this approach is necessary as ExtBuf contains reference to thread local storage,
// and LocalKey doesn't allow any references to exist outside of access callback
let fixed = ringal!{@ext, 64, |mut extendable| {
let _ = extendable.write(b"hello world!").unwrap();
// it's ok to return FixedBuf(Mut) from callback
extendable.finalize()
}}.unwrap();
println!("bytes written: {}", fixed.len());
§Safety Considerations
While unsafe
code is used for performance reasons, the public API is designed to be safe.
Significant efforts have been made to ensure no undefined behavior occurs, offering a safe
experience for end-users.
Structs§
- ExtBuf
- Extendable buffer. It dynamically grows to accomodate any extra data beyond the current
capacity, given that there’s still some capacity available to allocator itself. Contains a
reference to allocator, thus it effectively locks the allocator, and prevents any further
allocations while any instance of this type exist. After the necessary data is written, the
buffer should be finalized in order to release allocator lock and make the underlying buffer
Send
- Fixed
Buf - Immutable and cheaply cloneable buffer
- Fixed
BufMut - Fixed length (not growable) mutable buffer
- Generic
BufMut - Mutable fixed buffer, which is generic over its elements T, with
Vec<T>
like API - RingAl
- Ring Allocator, see crate level documentation on features and usage