| Crates.io | ringal |
| lib.rs | ringal |
| version | 0.4.0 |
| created_at | 2024-12-23 08:48:03.837744+00 |
| updated_at | 2024-12-27 16:31:29.502408+00 |
| description | Efficient ring allocator for short-lived buffers. |
| homepage | https://crates.io/crates/ringal |
| repository | https://github.com/bmuddha/ringal |
| max_upload_size | |
| id | 1492867 |
| size | 61,814 |
RingAl is a high-performance ring allocator designed for quick buffer allocations that don't live long. It uses a circular allocation method for fast memory management, which is perfect for short-lived buffers. For best performance, buffers should be freed quickly to avoid slowing things down by jamming the allocator.
N bytes.N.Arc.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 t 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.
Guards help manage memory by indicating:
When you request memory, RingAl checks the current guard and does one of the following:
None if it can't satisfy the request, even after merging. allocator
|
v
-----------------------------------------------------------------------------------
| head canary | N bytes | guard1 | L bytes | guard2 | M bytes | ... | tail canary |
-----------------------------------------------------------------------------------
| ^ | ^ | ^ | ^
| | | | | | | |
---------------------- ---------------- ---------------- ------------
^ |
| |
-------------------------------------------------------------------------
Note: Head and Tail canaries are fixed regular guards, with the exception that they always persist, and Tail guard always points to Head, forming a ring.
Vec<u8>, unless it hits capacity limits.Vec<T> like fixed capacity buffers, these can be allocated from
the same backing store as regular u8 buffersFor more details, check the RingAl Documentation.
tls (Thread-Local Storage): Adds thread-local storage, simplifying allocation calls, but might slow things slightly due to RefCell usage.drop (Allocator Deallocation): Normally, the allocator lives for the app's duration, but this feature allows early deallocation when needed. It waits for all allocations to finish before freeing memory to prevent leaks, blocks the thread if there're active allocations.let mut allocator = RingAl::new(1024); // Create an allocator with initial size
let mut buffer = allocator.extendable(64).unwrap();
let msg = b"hello world, this message is longer than the allocated capacity but will grow as needed.";
let size = buffer.write(msg).unwrap();
let fixed = buffer.finalize();
assert_eq!(fixed.as_ref(), msg);
assert_eq!(fixed.len(), size);
let mut allocator = RingAl::new(1024); // Create an allocator with initial size
let mut buffer = allocator.fixed(256).unwrap();
let size = buffer.write(b"hello world, this message is relatively short").unwrap();
assert_eq!(buffer.len(), size);
assert_eq!(buffer.spare(), 256 - size);
let mut allocator = RingAl::new(1024); // Create an allocator with initial size
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);
let mut allocator = RingAl::new(1024); // Create an allocator with initial size
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 = rx.recv().unwrap();
let readonly = buffer.freeze();
let mut handles = Vec::with_capacity(16);
for i in 0..16 {
let (tx, rx) = channel();
let h = std::thread::spawn(move || {
let msg = rx.recv().unwrap();
let msg = std::str::from_utf8(&msg[..]).unwrap();
println!("{i}. {msg}");
});
tx.send(readonly.clone());
handles.push(h);
}
for h in handles {
h.join();
}
});
tx.send(buffer);
handle.join();
// 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, |extendable| {
let _ = extendable.write(b"hello world!").unwrap();
// it's ok to return FixedBuf(Mut) from callback
extendable.finalize()
}};
println!("bytes written: {}", fixed.len());
Comparisons between RingAl and the system allocator (Vec<u8>) show performance gains in various scenarios.
| Iterations | Buffer Size | Max Buffers | ringal (ms) | vec (ms) | Improvement |
|---|---|---|---|---|---|
| 10,000,000 | 64 | 64 | 696.7 | 998.0 | 1.43x |
| 10,000,000 | 1,024 | 64 | 923.4 | 2,062.0 | 2.23x |
| 10,000,000 | 1,024 | 1,024 | 913.7 | 1,622.0 | 1.77x |
| 1,000,000 | 65,536 | 64 | 932.6 | 1,718.0 | 1.84x |
| 1,000,000 | 131,072 | 1,024 | 1,709.0 | 3,960.0 | 2.32x |
| Iterations | Buffer Size | Max Buffers | ringal (ms) | vec (ms) | Improvement |
|---|---|---|---|---|---|
| 10,000,000 | 64 | 64 | 834.0 | 1,120.0 | 1.34x |
| 10,000,000 | 1,024 | 64 | 1,164.0 | 3,228.0 | 2.77x |
| 10,000,000 | 1,024 | 1,024 | 1,205.0 | 3,044.0 | 2.53x |
| 1,000,000 | 65,536 | 64 | 1,958.0 | 3,646.0 | 1.86x |
| 1,000,000 | 131,072 | 1,024 | 3,588.0 | 9,008.0 | 2.51x |
RingAl consistently beats Vec<u8> in all cases.Benchmarks done with hyperfine, running each case 10 times. Shown times are averages.
RingAl doesn't rely on any third-party crates, only the Rust standard library.
The implementation uses a lot of unsafe Rust, this is an allocator after all, so raw pointers are inevitable (some parts of code look like they were written in C :)). A lot of effort was put into ensuring that the code is safe from UB and data races, and the API is safe to use. Nonetheless all contributions to making it even safer are always welcome.