perf(allocator): Allocator::used_bytes do not use chunk iterator (#21771)

Add a method `Arena::used_bytes` which `Allocator::used_bytes` utilizes. Re-implement it to avoid using `ChunkRawIter` iterator, which is slightly more performant.
This commit is contained in:
overlookmotel committed 2026-04-26 21:45:42 +00:00
1 parent 5199c897a1
commit 7896bd086c
5 files changed
+88 -11

No files matched your search

+5 -7
View File
@@ -569,14 +569,12 @@ impl Allocator {
/// [`Vec`]: crate::Vec
/// [`StringBuilder`]: crate::StringBuilder
/// [`HashMap`]: crate::HashMap
//
// `#[inline(always)]` because just delegates to `Arena` method
#[expect(clippy::inline_always)]
#[inline(always)]
pub fn used_bytes(&self) -> usize {
let mut bytes = 0;
// SAFETY: No allocations are made while `chunks_iter` is alive. No data is read from the chunks.
let chunks_iter = unsafe { self.arena.iter_allocated_chunks_raw() };
for (_, size) in chunks_iter {
bytes += size;
}
bytes
self.arena.used_bytes()
}
/// Get inner [`Arena`].
+45
View File
@@ -163,6 +163,51 @@ impl<const MIN_ALIGN: usize> Arena<MIN_ALIGN> {
}
total
}
/// Calculate the total size of data used in this [`Arena`], in bytes.
///
/// This is the total amount of memory that has been *used* in the `Arena`, NOT the amount of
/// memory the `Arena` owns. If you want the latter, use [`allocated_bytes`] instead.
///
/// The result includes:
/// * Padding bytes within objects allocated in the arena.
/// * Padding bytes to preserve alignment of types between objects which have been allocated.
///
/// [`allocated_bytes`]: Self::allocated_bytes
pub fn used_bytes(&self) -> usize {
let mut footer_ptr = self.current_chunk_footer_ptr.get();
// SAFETY: `self.current_chunk_footer_ptr` always points to a valid `ChunkFooter`
let footer = unsafe { footer_ptr.as_ref() };
// If `Arena` owns no memory, return 0
if footer.is_empty() {
return 0;
}
// Get current chunk's used bytes from pointers in `self`, not from the `ChunkFooter`
let end_ptr = footer_ptr.cast::<u8>();
// SAFETY: `cursor_ptr` is always before or equal to `end_ptr`
let mut total = unsafe { end_ptr.offset_from_unsigned(self.cursor_ptr.get()) };
// Add used bytes from previous chunks, getting pointer from their `ChunkFooter`s
footer_ptr = footer.previous_chunk_footer_ptr.get();
// SAFETY: `footer_ptr` always points to a valid `ChunkFooter`
while !unsafe { footer_ptr.as_ref() }.is_empty() {
// SAFETY: `footer_ptr` always points to a valid `ChunkFooter`
let footer = unsafe { footer_ptr.as_ref() };
let cursor_ptr = footer.cursor_ptr.get();
let end_ptr = footer_ptr.cast::<u8>();
debug_assert!(cursor_ptr <= end_ptr);
// SAFETY: `cursor_ptr` is always before or equal to `end_ptr`, and both are within the same allocation
total += unsafe { end_ptr.offset_from_unsigned(cursor_ptr) };
footer_ptr = footer.previous_chunk_footer_ptr.get();
}
total
}
}
/// An iterator over each chunk of allocated memory that an arena has allocated into.
+1 -1
View File
@@ -270,7 +270,7 @@ struct ChunkFooter {
/// Bump allocation cursor, valid only for retired (non-current) chunks.
/// This field is written when a chunk is retired (when a new chunk is created).
/// Allocation methods use `Arena::cursor_ptr` instead, which is the authoritative pointer for current chunk.
/// This field is only used in `ChunkIter` and `ChunkRawIter` iterators.
/// This field is only used in `ChunkIter` and `ChunkRawIter` iterators, and `used_bytes` method.
cursor_ptr: Cell<NonNull<u8>>,
}
+11 -3
View File
@@ -24,7 +24,7 @@ fn arena_not_sync() {}
// Uses private `DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER`, `OVERHEAD`, and `TYPICAL_PAGE_SIZE`
#[test]
fn allocated_bytes() {
fn allocated_and_used_bytes() {
fn chunk_count<const MIN_ALIGN: usize>(b: &mut Arena<MIN_ALIGN>) -> usize {
b.iter_allocated_chunks().count()
}
@@ -44,30 +44,36 @@ fn allocated_bytes() {
// Empty arena owns no chunks
assert_eq!(b.allocated_bytes(), 0);
assert_eq!(b.used_bytes(), 0);
assert_eq!(chunk_count(&mut b), 0);
// First allocation creates the first chunk
b.alloc(0u8);
assert_eq!(b.allocated_bytes(), DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER);
assert_eq!(b.used_bytes(), 1);
assert_eq!(chunk_count(&mut b), 1);
// Reset with a single chunk: The chunk is retained (capacity unchanged), but the cursor is reset
b.reset();
assert_eq!(b.allocated_bytes(), DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER);
assert_eq!(b.used_bytes(), 0);
assert_eq!(chunk_count(&mut b), 1);
// Allocations that fit in the current chunk don't grow the total
// Allocations that fit in the current chunk don't grow the allocated total, but do grow `used_bytes`
let small = Layout::from_size_align(64, 1).unwrap();
b.alloc_layout(small);
b.alloc_layout(small);
assert_eq!(b.allocated_bytes(), DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER);
assert_eq!(b.used_bytes(), small.size() * 2);
assert_eq!(chunk_count(&mut b), 1);
// A request which cannot be served in first chunk forces creation of a second chunk
// A request which cannot be served in first chunk forces creation of a second chunk.
// Chunk 1 is retired with its current `used_bytes` (`2 * 64`); chunk 2 holds the `big` allocation.
let big = Layout::from_size_align(DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER, 1).unwrap();
b.alloc_layout(big);
let two_chunks = b.allocated_bytes();
assert_eq!(two_chunks, DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER + SECOND_CHUNK);
assert_eq!(b.used_bytes(), small.size() * 2 + big.size());
assert_eq!(chunk_count(&mut b), 2);
// A request larger than every chunk seen so far forces a third chunk.
@@ -77,6 +83,7 @@ fn allocated_bytes() {
b.alloc_layout(huge);
let three_chunks = b.allocated_bytes();
assert_eq!(three_chunks, DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER + SECOND_CHUNK + THIRD_CHUNK);
assert_eq!(b.used_bytes(), small.size() * 2 + big.size() + huge.size());
assert_eq!(chunk_count(&mut b), 3);
// `reset` with multiple chunks deallocates every chunk except the most recent (which is also the largest),
@@ -84,6 +91,7 @@ fn allocated_bytes() {
b.reset();
let after_reset = b.allocated_bytes();
assert_eq!(after_reset, THIRD_CHUNK);
assert_eq!(b.used_bytes(), 0);
assert_eq!(chunk_count(&mut b), 1);
}
@@ -273,4 +273,30 @@ quickcheck! {
}
}
fn used_bytes_tracking(allocs: Vec<usize>) -> () {
let mut b = Arena::new();
let mut slice_bytes = 0;
for len in allocs {
const MAX_LEN: usize = 512;
let len = len % MAX_LEN;
b.alloc_slice_fill_copy(len, 0u8);
slice_bytes += len;
// `Arena<1>` with `align == 1` allocations: cursor advances by exactly `len` per call,
// so total used bytes equal the sum of requested sizes
let used_bytes = b.used_bytes();
assert_eq!(used_bytes, slice_bytes);
// `used_bytes <= allocated_bytes` (capacity >= used)
assert!(used_bytes <= b.allocated_bytes());
// `used_bytes` agrees with summing each chunk's used portion via `iter_allocated_chunks`
let from_iter = b.iter_allocated_chunks().map(<[_]>::len).sum();
assert_eq!(used_bytes, from_iter);
}
// After `reset`, the surviving chunk's cursor is back at its end - 0 bytes used
b.reset();
assert_eq!(b.used_bytes(), 0);
}
}