mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
refactor(allocator): move Allocator into own module (#8656)
Pure refactor. `oxc_allocator` crate is getting quite crowded now. Move `Allocator` into a separate file.
This commit is contained in:
parent
0f85bc6918
commit
ae8db53d65
2 changed files with 462 additions and 452 deletions
460
crates/oxc_allocator/src/allocator.rs
Normal file
460
crates/oxc_allocator/src/allocator.rs
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
use bumpalo::Bump;
|
||||
|
||||
/// A bump-allocated memory arena.
|
||||
///
|
||||
/// # Anatomy of an Allocator
|
||||
///
|
||||
/// [`Allocator`] is flexibly sized. It grows as required as you allocate data into it.
|
||||
///
|
||||
/// To do that, an [`Allocator`] consists of multiple memory chunks.
|
||||
///
|
||||
/// [`Allocator::new`] creates a new allocator without any chunks. When you first allocate an object
|
||||
/// into it, it will lazily create an initial chunk, the size of which is determined by the size of that
|
||||
/// first allocation.
|
||||
///
|
||||
/// As more data is allocated into the [`Allocator`], it will likely run out of capacity. At that point,
|
||||
/// a new memory chunk is added, and further allocations will use this new chunk (until it too runs out
|
||||
/// of capacity, and *another* chunk is added).
|
||||
///
|
||||
/// The data from the 1st chunk is not copied into the 2nd one. It stays where it is, which means
|
||||
/// `&` or `&mut` references to data in the first chunk remain valid. This is unlike e.g. `Vec` which
|
||||
/// copies all existing data when it grows.
|
||||
///
|
||||
/// Each chunk is at least double the size of the last one, so growth in capacity is exponential.
|
||||
///
|
||||
/// [`Allocator::reset`] keeps only the last chunk (the biggest one), and discards any other chunks,
|
||||
/// returning their memory to the global allocator. The last chunk has its cursor rewound back to
|
||||
/// the start, so it's empty, ready to be re-used for allocating more data.
|
||||
///
|
||||
/// # Recycling allocators
|
||||
///
|
||||
/// For good performance, it's ideal to create an [`Allocator`], and re-use it over and over, rather than
|
||||
/// repeatedly creating and dropping [`Allocator`]s.
|
||||
///
|
||||
/// ```
|
||||
/// // This is good!
|
||||
/// use oxc_allocator::Allocator;
|
||||
/// let mut allocator = Allocator::new();
|
||||
///
|
||||
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
|
||||
/// for i in 0..100 {
|
||||
/// do_stuff(i, &allocator);
|
||||
/// // Reset the allocator, freeing the memory used by `do_stuff`
|
||||
/// allocator.reset();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // DON'T DO THIS!
|
||||
/// # use oxc_allocator::Allocator;
|
||||
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
|
||||
/// for i in 0..100 {
|
||||
/// let allocator = Allocator::new();
|
||||
/// do_stuff(i, &allocator);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // DON'T DO THIS EITHER!
|
||||
/// # use oxc_allocator::Allocator;
|
||||
/// # let allocator = Allocator::new();
|
||||
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
|
||||
/// for i in 0..100 {
|
||||
/// do_stuff(i, &allocator);
|
||||
/// // We haven't reset the allocator, so we haven't freed the memory used by `do_stuff`.
|
||||
/// // The allocator will grow and grow, consuming more and more memory.
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Why is re-using an [`Allocator`] good for performance?
|
||||
///
|
||||
/// 3 reasons:
|
||||
///
|
||||
/// #### 1. Avoid expensive system calls
|
||||
///
|
||||
/// Creating an [`Allocator`] is a fairly expensive operation as it involves a call into global allocator,
|
||||
/// which in turn will likely make a system call. Ditto when the [`Allocator`] is dropped.
|
||||
/// Re-using an existing [`Allocator`] avoids these costs.
|
||||
///
|
||||
/// #### 2. CPU cache
|
||||
///
|
||||
/// Re-using an existing allocator means you're re-using the same block of memory. If that memory was
|
||||
/// recently accessed, it's likely to be warm in the CPU cache, so memory accesses will be much faster
|
||||
/// than accessing "cold" sections of main memory.
|
||||
///
|
||||
/// This can have a very significant positive impact on performance.
|
||||
///
|
||||
/// #### 3. Capacity stabilization
|
||||
///
|
||||
/// The most efficient [`Allocator`] is one with only 1 chunk which has sufficient capacity for
|
||||
/// everything you're going to allocate into it.
|
||||
///
|
||||
/// Why?
|
||||
///
|
||||
/// 1. Every allocation will occur without the allocator needing to grow.
|
||||
///
|
||||
/// 2. This makes the "is there sufficient capacity to allocate this?" check in [`alloc`] completely
|
||||
/// predictable (the answer is always "yes"). The CPU's branch predictor swiftly learns this,
|
||||
/// speeding up operation.
|
||||
///
|
||||
/// 3. When the [`Allocator`] is reset, there are no excess chunks to discard, so no system calls.
|
||||
///
|
||||
/// Because [`reset`] keeps only the biggest chunk (see above), re-using the same [`Allocator`]
|
||||
/// for multiple similar workloads will result in the [`Allocator`] swiftly stabilizing at a capacity
|
||||
/// which is sufficient to service those workloads with a single chunk.
|
||||
///
|
||||
/// If workload is completely uniform, it reaches stable state on the 3rd round.
|
||||
///
|
||||
/// ```
|
||||
/// # use oxc_allocator::Allocator;
|
||||
/// let mut allocator = Allocator::new();
|
||||
///
|
||||
/// fn workload(allocator: &Allocator) {
|
||||
/// // Allocate 4 MB of data in small chunks
|
||||
/// for i in 0..1_000_000u32 {
|
||||
/// allocator.alloc(i);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // 1st round
|
||||
/// workload(&allocator);
|
||||
///
|
||||
/// // `allocator` has capacity for 4 MB data, but split into many chunks.
|
||||
/// // `reset` throws away all chunks except the last one which will be approx 2 MB.
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// // 2nd round
|
||||
/// workload(&allocator);
|
||||
///
|
||||
/// // `workload` filled the 2 MB chunk, so a 2nd chunk was created of double the size (4 MB).
|
||||
/// // `reset` discards the smaller chunk, leaving only a single 4 MB chunk.
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// // 3rd round
|
||||
/// // `allocator` now has sufficient capacity for all allocations in a single 4 MB chunk.
|
||||
/// workload(&allocator);
|
||||
///
|
||||
/// // `reset` has no chunks to discard. It keeps the single 4 MB chunk. No system calls.
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// // More rounds
|
||||
/// // All serviced without needing to grow the allocator, and with no system calls.
|
||||
/// for _ in 0..100 {
|
||||
/// workload(&allocator);
|
||||
/// allocator.reset();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # No `Drop`s
|
||||
///
|
||||
/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop).
|
||||
/// Memory is released in bulk when the allocator is dropped, without dropping the individual
|
||||
/// objects in the arena.
|
||||
///
|
||||
/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
|
||||
/// which own memory allocations outside the arena.
|
||||
///
|
||||
/// Static checks make this impossible to do. [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`],
|
||||
/// [`HashMap::new_in`], and all other methods which store data in the arena will refuse to compile
|
||||
/// if called with a [`Drop`] type.
|
||||
///
|
||||
/// ```ignore
|
||||
/// use oxc_allocator::{Allocator, Box};
|
||||
///
|
||||
/// let allocator = Allocator::new();
|
||||
///
|
||||
/// struct Foo {
|
||||
/// pub a: i32
|
||||
/// }
|
||||
///
|
||||
/// impl std::ops::Drop for Foo {
|
||||
/// fn drop(&mut self) {}
|
||||
/// }
|
||||
///
|
||||
/// // This will fail to compile because `Foo` implements `Drop`
|
||||
/// let foo = Box::new_in(Foo { a: 0 }, &allocator);
|
||||
///
|
||||
/// struct Bar {
|
||||
/// v: std::vec::Vec<u8>,
|
||||
/// }
|
||||
///
|
||||
/// // This will fail to compile because `Bar` contains a `std::vec::Vec`, and it implements `Drop`
|
||||
/// let bar = Box::new_in(Bar { v: vec![1, 2, 3] }, &allocator);
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Consumers of the [`oxc` umbrella crate](https://crates.io/crates/oxc) pass
|
||||
/// [`Allocator`] references to other tools.
|
||||
///
|
||||
/// ```ignore
|
||||
/// use oxc::{allocator::Allocator, parser::Parser, span::SourceType};
|
||||
///
|
||||
/// let allocator = Allocator::default();
|
||||
/// let parsed = Parser::new(&allocator, "let x = 1;", SourceType::default());
|
||||
/// assert!(parsed.errors.is_empty());
|
||||
/// ```
|
||||
///
|
||||
/// [`reset`]: Allocator::reset
|
||||
/// [`alloc`]: Allocator::alloc
|
||||
/// [`Box::new_in`]: crate::Box::new_in
|
||||
/// [`Vec::new_in`]: crate::Vec::new_in
|
||||
/// [`HashMap::new_in`]: crate::HashMap::new_in
|
||||
#[derive(Default)]
|
||||
pub struct Allocator {
|
||||
bump: Bump,
|
||||
}
|
||||
|
||||
impl Allocator {
|
||||
/// Create a new [`Allocator`] with no initial capacity.
|
||||
///
|
||||
/// This method does not reserve any memory to back the allocator. Memory for allocator's initial
|
||||
/// chunk will be reserved lazily, when you make the first allocation into this [`Allocator`]
|
||||
/// (e.g. with [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`], [`HashMap::new_in`]).
|
||||
///
|
||||
/// If you can estimate the amount of memory the allocator will require to fit what you intend to
|
||||
/// allocate into it, it is generally preferable to create that allocator with [`with_capacity`],
|
||||
/// which reserves that amount of memory upfront. This will avoid further system calls to allocate
|
||||
/// further chunks later on. This point is less important if you're re-using the allocator multiple
|
||||
/// times.
|
||||
///
|
||||
/// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
|
||||
///
|
||||
/// [`with_capacity`]: Allocator::with_capacity
|
||||
/// [`Box::new_in`]: crate::Box::new_in
|
||||
/// [`Vec::new_in`]: crate::Vec::new_in
|
||||
/// [`HashMap::new_in`]: crate::HashMap::new_in
|
||||
//
|
||||
// `#[inline(always)]` because just delegates to `bumpalo` method
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn new() -> Self {
|
||||
Self { bump: Bump::new() }
|
||||
}
|
||||
|
||||
/// Create a new [`Allocator`] with specified capacity.
|
||||
///
|
||||
/// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
|
||||
//
|
||||
// `#[inline(always)]` because just delegates to `bumpalo` method
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self { bump: Bump::with_capacity(capacity) }
|
||||
}
|
||||
|
||||
/// Allocate an object in this [`Allocator`] and return an exclusive reference to it.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if reserving space for `T` fails.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::Allocator;
|
||||
///
|
||||
/// let allocator = Allocator::default();
|
||||
/// let x = allocator.alloc([1u8; 20]);
|
||||
/// assert_eq!(x, &[1u8; 20]);
|
||||
/// ```
|
||||
//
|
||||
// `#[inline(always)]` because this is a very hot path and `Bump::alloc` is a very small function.
|
||||
// We always want it to be inlined.
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn alloc<T>(&self, val: T) -> &mut T {
|
||||
const { assert!(!std::mem::needs_drop::<T>(), "Cannot allocate Drop type in arena") };
|
||||
|
||||
self.bump.alloc(val)
|
||||
}
|
||||
|
||||
/// Copy a string slice into this [`Allocator`] and return a reference to it.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if reserving space for the string fails.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::Allocator;
|
||||
/// let allocator = Allocator::default();
|
||||
/// let hello = allocator.alloc_str("hello world");
|
||||
/// assert_eq!(hello, "hello world");
|
||||
/// ```
|
||||
//
|
||||
// `#[inline(always)]` because this is a hot path and `Bump::alloc_str` is a very small function.
|
||||
// We always want it to be inlined.
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn alloc_str<'alloc>(&'alloc self, src: &str) -> &'alloc mut str {
|
||||
self.bump.alloc_str(src)
|
||||
}
|
||||
|
||||
/// Reset this allocator.
|
||||
///
|
||||
/// Performs mass deallocation on everything allocated in this arena by resetting the pointer
|
||||
/// into the underlying chunk of memory to the start of the chunk.
|
||||
/// Does not run any `Drop` implementations on deallocated objects.
|
||||
///
|
||||
/// If this arena has allocated multiple chunks to bump allocate into, then the excess chunks
|
||||
/// are returned to the global allocator.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::Allocator;
|
||||
///
|
||||
/// let mut allocator = Allocator::default();
|
||||
///
|
||||
/// // Allocate a bunch of things.
|
||||
/// {
|
||||
/// for i in 0..100 {
|
||||
/// allocator.alloc(i);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Reset the arena.
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// // Allocate some new things in the space previously occupied by the
|
||||
/// // original things.
|
||||
/// for j in 200..400 {
|
||||
/// allocator.alloc(j);
|
||||
/// }
|
||||
/// ```
|
||||
//
|
||||
// `#[inline(always)]` because it just delegates to `bumpalo`
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn reset(&mut self) {
|
||||
self.bump.reset();
|
||||
}
|
||||
|
||||
/// Calculate the total capacity of this [`Allocator`] including all chunks, in bytes.
|
||||
///
|
||||
/// Note: This is the total amount of memory the [`Allocator`] owns NOT the total size of data
|
||||
/// that's been allocated in it. If you want the latter, use [`used_bytes`] instead.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::Allocator;
|
||||
///
|
||||
/// let capacity = 64 * 1024; // 64 KiB
|
||||
/// let mut allocator = Allocator::with_capacity(capacity);
|
||||
/// allocator.alloc(123u64); // 8 bytes
|
||||
///
|
||||
/// // Result is the capacity (64 KiB), not the size of allocated data (8 bytes).
|
||||
/// // `Allocator::with_capacity` may allocate a bit more than requested.
|
||||
/// assert!(allocator.capacity() >= capacity);
|
||||
/// ```
|
||||
///
|
||||
/// [`used_bytes`]: Allocator::used_bytes
|
||||
//
|
||||
// `#[inline(always)]` because it just delegates to `bumpalo`
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.bump.allocated_bytes()
|
||||
}
|
||||
|
||||
/// Calculate the total size of data used in this [`Allocator`], in bytes.
|
||||
///
|
||||
/// This is the total amount of memory that has been *used* in the [`Allocator`], NOT the amount of
|
||||
/// memory the [`Allocator`] owns. If you want the latter, use [`capacity`] instead.
|
||||
///
|
||||
/// The result includes:
|
||||
///
|
||||
/// 1. Padding bytes between objects which have been allocated to preserve alignment of types
|
||||
/// where they have different alignments or have larger-than-typical alignment.
|
||||
/// 2. Excess capacity in [`Vec`]s, [`String`]s and [`HashMap`]s.
|
||||
/// 3. Objects which were allocated but later dropped. [`Allocator`] does not re-use allocations,
|
||||
/// so anything which is allocated into arena continues to take up "dead space", even after it's
|
||||
/// no longer referenced anywhere.
|
||||
/// 4. "Dead space" left over where a [`Vec`], [`String`] or [`HashMap`] has grown and had to make
|
||||
/// a new allocation to accommodate its new larger size. Its old allocation continues to take up
|
||||
/// "dead" space in the allocator, unless it was the most recent allocation.
|
||||
///
|
||||
/// In practice, this almost always means that the result returned from this function will be an
|
||||
/// over-estimate vs the amount of "live" data in the arena.
|
||||
///
|
||||
/// However, if you are using the result of this method to create a new `Allocator` to clone
|
||||
/// an AST into, it is theoretically possible (though very unlikely) that it may be a slight
|
||||
/// under-estimate of the capacity required in new allocator to clone the AST into, depending
|
||||
/// on the order that `&str`s were allocated into arena in parser vs the order they get allocated
|
||||
/// during cloning. The order allocations are made in affects the amount of padding bytes required.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::{Allocator, Vec};
|
||||
///
|
||||
/// let capacity = 64 * 1024; // 64 KiB
|
||||
/// let mut allocator = Allocator::with_capacity(capacity);
|
||||
///
|
||||
/// allocator.alloc(1u8); // 1 byte with alignment 1
|
||||
/// allocator.alloc(2u8); // 1 byte with alignment 1
|
||||
/// allocator.alloc(3u64); // 8 bytes with alignment 8
|
||||
///
|
||||
/// // Only 10 bytes were allocated, but 16 bytes were used, in order to align `3u64` on 8
|
||||
/// assert_eq!(allocator.used_bytes(), 16);
|
||||
///
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// let mut vec = Vec::<u64>::with_capacity_in(2, &allocator);
|
||||
///
|
||||
/// // Allocate something else, so `vec`'s allocation is not the most recent
|
||||
/// allocator.alloc(123u64);
|
||||
///
|
||||
/// // `vec` has to grow beyond it's initial capacity
|
||||
/// vec.extend([1, 2, 3, 4]);
|
||||
///
|
||||
/// // `vec` takes up 32 bytes, and `123u64` takes up 8 bytes = 40 total.
|
||||
/// // But there's an additional 16 bytes consumed for `vec`'s original capacity of 2,
|
||||
/// // which is still using up space
|
||||
/// assert_eq!(allocator.used_bytes(), 56);
|
||||
/// ```
|
||||
///
|
||||
/// [`capacity`]: Allocator::capacity
|
||||
/// [`Vec`]: crate::Vec
|
||||
/// [`String`]: crate::String
|
||||
/// [`HashMap`]: crate::HashMap
|
||||
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.bump.iter_allocated_chunks_raw() };
|
||||
for (_, size) in chunks_iter {
|
||||
bytes += size;
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Get inner [`bumpalo::Bump`].
|
||||
///
|
||||
/// This method is not public. We don't want to expose `Bump` to user.
|
||||
/// The fact that we're using `bumpalo` is an internal implementation detail.
|
||||
//
|
||||
// `#[inline(always)]` because it's a no-op
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub(crate) fn bump(&self) -> &Bump {
|
||||
&self.bump
|
||||
}
|
||||
}
|
||||
|
||||
/// SAFETY: Not actually safe, but for enabling `Send` for downstream crates.
|
||||
unsafe impl Send for Allocator {}
|
||||
/// SAFETY: Not actually safe, but for enabling `Sync` for downstream crates.
|
||||
unsafe impl Sync for Allocator {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Allocator;
|
||||
|
||||
#[test]
|
||||
fn test_api() {
|
||||
let mut allocator = Allocator::default();
|
||||
{
|
||||
let array = allocator.alloc([123; 10]);
|
||||
assert_eq!(array, &[123; 10]);
|
||||
let str = allocator.alloc_str("hello");
|
||||
assert_eq!(str, "hello");
|
||||
}
|
||||
allocator.reset();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,9 +14,8 @@
|
|||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use bumpalo::Bump;
|
||||
|
||||
mod address;
|
||||
mod allocator;
|
||||
mod allocator_api2;
|
||||
mod boxed;
|
||||
mod clone_in;
|
||||
|
|
@ -26,459 +25,10 @@ pub mod string;
|
|||
mod vec;
|
||||
|
||||
pub use address::{Address, GetAddress};
|
||||
pub use allocator::Allocator;
|
||||
pub use boxed::Box;
|
||||
pub use clone_in::CloneIn;
|
||||
pub use convert::{FromIn, IntoIn};
|
||||
pub use hash_map::HashMap;
|
||||
pub use string::String;
|
||||
pub use vec::Vec;
|
||||
|
||||
/// A bump-allocated memory arena.
|
||||
///
|
||||
/// # Anatomy of an Allocator
|
||||
///
|
||||
/// [`Allocator`] is flexibly sized. It grows as required as you allocate data into it.
|
||||
///
|
||||
/// To do that, an [`Allocator`] consists of multiple memory chunks.
|
||||
///
|
||||
/// [`Allocator::new`] creates a new allocator without any chunks. When you first allocate an object
|
||||
/// into it, it will lazily create an initial chunk, the size of which is determined by the size of that
|
||||
/// first allocation.
|
||||
///
|
||||
/// As more data is allocated into the [`Allocator`], it will likely run out of capacity. At that point,
|
||||
/// a new memory chunk is added, and further allocations will use this new chunk (until it too runs out
|
||||
/// of capacity, and *another* chunk is added).
|
||||
///
|
||||
/// The data from the 1st chunk is not copied into the 2nd one. It stays where it is, which means
|
||||
/// `&` or `&mut` references to data in the first chunk remain valid. This is unlike e.g. `Vec` which
|
||||
/// copies all existing data when it grows.
|
||||
///
|
||||
/// Each chunk is at least double the size of the last one, so growth in capacity is exponential.
|
||||
///
|
||||
/// [`Allocator::reset`] keeps only the last chunk (the biggest one), and discards any other chunks,
|
||||
/// returning their memory to the global allocator. The last chunk has its cursor rewound back to
|
||||
/// the start, so it's empty, ready to be re-used for allocating more data.
|
||||
///
|
||||
/// # Recycling allocators
|
||||
///
|
||||
/// For good performance, it's ideal to create an [`Allocator`], and re-use it over and over, rather than
|
||||
/// repeatedly creating and dropping [`Allocator`]s.
|
||||
///
|
||||
/// ```
|
||||
/// // This is good!
|
||||
/// use oxc_allocator::Allocator;
|
||||
/// let mut allocator = Allocator::new();
|
||||
///
|
||||
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
|
||||
/// for i in 0..100 {
|
||||
/// do_stuff(i, &allocator);
|
||||
/// // Reset the allocator, freeing the memory used by `do_stuff`
|
||||
/// allocator.reset();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // DON'T DO THIS!
|
||||
/// # use oxc_allocator::Allocator;
|
||||
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
|
||||
/// for i in 0..100 {
|
||||
/// let allocator = Allocator::new();
|
||||
/// do_stuff(i, &allocator);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // DON'T DO THIS EITHER!
|
||||
/// # use oxc_allocator::Allocator;
|
||||
/// # let allocator = Allocator::new();
|
||||
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
|
||||
/// for i in 0..100 {
|
||||
/// do_stuff(i, &allocator);
|
||||
/// // We haven't reset the allocator, so we haven't freed the memory used by `do_stuff`.
|
||||
/// // The allocator will grow and grow, consuming more and more memory.
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Why is re-using an [`Allocator`] good for performance?
|
||||
///
|
||||
/// 3 reasons:
|
||||
///
|
||||
/// #### 1. Avoid expensive system calls
|
||||
///
|
||||
/// Creating an [`Allocator`] is a fairly expensive operation as it involves a call into global allocator,
|
||||
/// which in turn will likely make a system call. Ditto when the [`Allocator`] is dropped.
|
||||
/// Re-using an existing [`Allocator`] avoids these costs.
|
||||
///
|
||||
/// #### 2. CPU cache
|
||||
///
|
||||
/// Re-using an existing allocator means you're re-using the same block of memory. If that memory was
|
||||
/// recently accessed, it's likely to be warm in the CPU cache, so memory accesses will be much faster
|
||||
/// than accessing "cold" sections of main memory.
|
||||
///
|
||||
/// This can have a very significant positive impact on performance.
|
||||
///
|
||||
/// #### 3. Capacity stabilization
|
||||
///
|
||||
/// The most efficient [`Allocator`] is one with only 1 chunk which has sufficient capacity for
|
||||
/// everything you're going to allocate into it.
|
||||
///
|
||||
/// Why?
|
||||
///
|
||||
/// 1. Every allocation will occur without the allocator needing to grow.
|
||||
///
|
||||
/// 2. This makes the "is there sufficient capacity to allocate this?" check in [`alloc`] completely
|
||||
/// predictable (the answer is always "yes"). The CPU's branch predictor swiftly learns this,
|
||||
/// speeding up operation.
|
||||
///
|
||||
/// 3. When the [`Allocator`] is reset, there are no excess chunks to discard, so no system calls.
|
||||
///
|
||||
/// Because [`reset`] keeps only the biggest chunk (see above), re-using the same [`Allocator`]
|
||||
/// for multiple similar workloads will result in the [`Allocator`] swiftly stabilizing at a capacity
|
||||
/// which is sufficient to service those workloads with a single chunk.
|
||||
///
|
||||
/// If workload is completely uniform, it reaches stable state on the 3rd round.
|
||||
///
|
||||
/// ```
|
||||
/// # use oxc_allocator::Allocator;
|
||||
/// let mut allocator = Allocator::new();
|
||||
///
|
||||
/// fn workload(allocator: &Allocator) {
|
||||
/// // Allocate 4 MB of data in small chunks
|
||||
/// for i in 0..1_000_000u32 {
|
||||
/// allocator.alloc(i);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // 1st round
|
||||
/// workload(&allocator);
|
||||
///
|
||||
/// // `allocator` has capacity for 4 MB data, but split into many chunks.
|
||||
/// // `reset` throws away all chunks except the last one which will be approx 2 MB.
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// // 2nd round
|
||||
/// workload(&allocator);
|
||||
///
|
||||
/// // `workload` filled the 2 MB chunk, so a 2nd chunk was created of double the size (4 MB).
|
||||
/// // `reset` discards the smaller chunk, leaving only a single 4 MB chunk.
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// // 3rd round
|
||||
/// // `allocator` now has sufficient capacity for all allocations in a single 4 MB chunk.
|
||||
/// workload(&allocator);
|
||||
///
|
||||
/// // `reset` has no chunks to discard. It keeps the single 4 MB chunk. No system calls.
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// // More rounds
|
||||
/// // All serviced without needing to grow the allocator, and with no system calls.
|
||||
/// for _ in 0..100 {
|
||||
/// workload(&allocator);
|
||||
/// allocator.reset();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`reset`]: Allocator::reset
|
||||
/// [`alloc`]: Allocator::alloc
|
||||
///
|
||||
/// # No `Drop`s
|
||||
///
|
||||
/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop).
|
||||
/// Memory is released in bulk when the allocator is dropped, without dropping the individual
|
||||
/// objects in the arena.
|
||||
///
|
||||
/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
|
||||
/// which own memory allocations outside the arena.
|
||||
///
|
||||
/// Static checks make this impossible to do. [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`],
|
||||
/// [`HashMap::new_in`], and all other methods which store data in the arena will refuse to compile
|
||||
/// if called with a [`Drop`] type.
|
||||
///
|
||||
/// ```ignore
|
||||
/// use oxc_allocator::{Allocator, Box};
|
||||
///
|
||||
/// let allocator = Allocator::new();
|
||||
///
|
||||
/// struct Foo {
|
||||
/// pub a: i32
|
||||
/// }
|
||||
///
|
||||
/// impl std::ops::Drop for Foo {
|
||||
/// fn drop(&mut self) {}
|
||||
/// }
|
||||
///
|
||||
/// // This will fail to compile because `Foo` implements `Drop`
|
||||
/// let foo = Box::new_in(Foo { a: 0 }, &allocator);
|
||||
///
|
||||
/// struct Bar {
|
||||
/// v: std::vec::Vec<u8>,
|
||||
/// }
|
||||
///
|
||||
/// // This will fail to compile because `Bar` contains a `std::vec::Vec`, and it implements `Drop`
|
||||
/// let bar = Box::new_in(Bar { v: vec![1, 2, 3] }, &allocator);
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Consumers of the [`oxc` umbrella crate](https://crates.io/crates/oxc) pass
|
||||
/// [`Allocator`] references to other tools.
|
||||
///
|
||||
/// ```ignore
|
||||
/// use oxc::{allocator::Allocator, parser::Parser, span::SourceType};
|
||||
///
|
||||
/// let allocator = Allocator::default();
|
||||
/// let parsed = Parser::new(&allocator, "let x = 1;", SourceType::default());
|
||||
/// assert!(parsed.errors.is_empty());
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct Allocator {
|
||||
bump: Bump,
|
||||
}
|
||||
|
||||
impl Allocator {
|
||||
/// Create a new [`Allocator`] with no initial capacity.
|
||||
///
|
||||
/// This method does not reserve any memory to back the allocator. Memory for allocator's initial
|
||||
/// chunk will be reserved lazily, when you make the first allocation into this [`Allocator`]
|
||||
/// (e.g. with [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`], [`HashMap::new_in`]).
|
||||
///
|
||||
/// If you can estimate the amount of memory the allocator will require to fit what you intend to
|
||||
/// allocate into it, it is generally preferable to create that allocator with [`with_capacity`],
|
||||
/// which reserves that amount of memory upfront. This will avoid further system calls to allocate
|
||||
/// further chunks later on. This point is less important if you're re-using the allocator multiple
|
||||
/// times.
|
||||
///
|
||||
/// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
|
||||
///
|
||||
/// [`with_capacity`]: Allocator::with_capacity
|
||||
//
|
||||
// `#[inline(always)]` because just delegates to `bumpalo` method
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn new() -> Self {
|
||||
Self { bump: Bump::new() }
|
||||
}
|
||||
|
||||
/// Create a new [`Allocator`] with specified capacity.
|
||||
///
|
||||
/// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
|
||||
//
|
||||
// `#[inline(always)]` because just delegates to `bumpalo` method
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self { bump: Bump::with_capacity(capacity) }
|
||||
}
|
||||
|
||||
/// Allocate an object in this [`Allocator`] and return an exclusive reference to it.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if reserving space for `T` fails.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::Allocator;
|
||||
///
|
||||
/// let allocator = Allocator::default();
|
||||
/// let x = allocator.alloc([1u8; 20]);
|
||||
/// assert_eq!(x, &[1u8; 20]);
|
||||
/// ```
|
||||
//
|
||||
// `#[inline(always)]` because this is a very hot path and `Bump::alloc` is a very small function.
|
||||
// We always want it to be inlined.
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn alloc<T>(&self, val: T) -> &mut T {
|
||||
const { assert!(!std::mem::needs_drop::<T>(), "Cannot allocate Drop type in arena") };
|
||||
|
||||
self.bump.alloc(val)
|
||||
}
|
||||
|
||||
/// Copy a string slice into this [`Allocator`] and return a reference to it.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if reserving space for the string fails.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::Allocator;
|
||||
/// let allocator = Allocator::default();
|
||||
/// let hello = allocator.alloc_str("hello world");
|
||||
/// assert_eq!(hello, "hello world");
|
||||
/// ```
|
||||
//
|
||||
// `#[inline(always)]` because this is a hot path and `Bump::alloc_str` is a very small function.
|
||||
// We always want it to be inlined.
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn alloc_str<'alloc>(&'alloc self, src: &str) -> &'alloc mut str {
|
||||
self.bump.alloc_str(src)
|
||||
}
|
||||
|
||||
/// Reset this allocator.
|
||||
///
|
||||
/// Performs mass deallocation on everything allocated in this arena by resetting the pointer
|
||||
/// into the underlying chunk of memory to the start of the chunk.
|
||||
/// Does not run any `Drop` implementations on deallocated objects.
|
||||
///
|
||||
/// If this arena has allocated multiple chunks to bump allocate into, then the excess chunks
|
||||
/// are returned to the global allocator.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::Allocator;
|
||||
///
|
||||
/// let mut allocator = Allocator::default();
|
||||
///
|
||||
/// // Allocate a bunch of things.
|
||||
/// {
|
||||
/// for i in 0..100 {
|
||||
/// allocator.alloc(i);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Reset the arena.
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// // Allocate some new things in the space previously occupied by the
|
||||
/// // original things.
|
||||
/// for j in 200..400 {
|
||||
/// allocator.alloc(j);
|
||||
/// }
|
||||
/// ```
|
||||
//
|
||||
// `#[inline(always)]` because it just delegates to `bumpalo`
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn reset(&mut self) {
|
||||
self.bump.reset();
|
||||
}
|
||||
|
||||
/// Calculate the total capacity of this [`Allocator`] including all chunks, in bytes.
|
||||
///
|
||||
/// Note: This is the total amount of memory the [`Allocator`] owns NOT the total size of data
|
||||
/// that's been allocated in it. If you want the latter, use [`used_bytes`] instead.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::Allocator;
|
||||
///
|
||||
/// let capacity = 64 * 1024; // 64 KiB
|
||||
/// let mut allocator = Allocator::with_capacity(capacity);
|
||||
/// allocator.alloc(123u64); // 8 bytes
|
||||
///
|
||||
/// // Result is the capacity (64 KiB), not the size of allocated data (8 bytes).
|
||||
/// // `Allocator::with_capacity` may allocate a bit more than requested.
|
||||
/// assert!(allocator.capacity() >= capacity);
|
||||
/// ```
|
||||
///
|
||||
/// [`used_bytes`]: Allocator::used_bytes
|
||||
//
|
||||
// `#[inline(always)]` because it just delegates to `bumpalo`
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.bump.allocated_bytes()
|
||||
}
|
||||
|
||||
/// Calculate the total size of data used in this [`Allocator`], in bytes.
|
||||
///
|
||||
/// This is the total amount of memory that has been *used* in the [`Allocator`], NOT the amount of
|
||||
/// memory the [`Allocator`] owns. If you want the latter, use [`capacity`] instead.
|
||||
///
|
||||
/// The result includes:
|
||||
///
|
||||
/// 1. Padding bytes between objects which have been allocated to preserve alignment of types
|
||||
/// where they have different alignments or have larger-than-typical alignment.
|
||||
/// 2. Excess capacity in [`Vec`]s, [`String`]s and [`HashMap`]s.
|
||||
/// 3. Objects which were allocated but later dropped. [`Allocator`] does not re-use allocations,
|
||||
/// so anything which is allocated into arena continues to take up "dead space", even after it's
|
||||
/// no longer referenced anywhere.
|
||||
/// 4. "Dead space" left over where a [`Vec`], [`String`] or [`HashMap`] has grown and had to make
|
||||
/// a new allocation to accommodate its new larger size. Its old allocation continues to take up
|
||||
/// "dead" space in the allocator, unless it was the most recent allocation.
|
||||
///
|
||||
/// In practice, this almost always means that the result returned from this function will be an
|
||||
/// over-estimate vs the amount of "live" data in the arena.
|
||||
///
|
||||
/// However, if you are using the result of this method to create a new `Allocator` to clone
|
||||
/// an AST into, it is theoretically possible (though very unlikely) that it may be a slight
|
||||
/// under-estimate of the capacity required in new allocator to clone the AST into, depending
|
||||
/// on the order that `&str`s were allocated into arena in parser vs the order they get allocated
|
||||
/// during cloning. The order allocations are made in affects the amount of padding bytes required.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use oxc_allocator::{Allocator, Vec};
|
||||
///
|
||||
/// let capacity = 64 * 1024; // 64 KiB
|
||||
/// let mut allocator = Allocator::with_capacity(capacity);
|
||||
///
|
||||
/// allocator.alloc(1u8); // 1 byte with alignment 1
|
||||
/// allocator.alloc(2u8); // 1 byte with alignment 1
|
||||
/// allocator.alloc(3u64); // 8 bytes with alignment 8
|
||||
///
|
||||
/// // Only 10 bytes were allocated, but 16 bytes were used, in order to align `3u64` on 8
|
||||
/// assert_eq!(allocator.used_bytes(), 16);
|
||||
///
|
||||
/// allocator.reset();
|
||||
///
|
||||
/// let mut vec = Vec::<u64>::with_capacity_in(2, &allocator);
|
||||
///
|
||||
/// // Allocate something else, so `vec`'s allocation is not the most recent
|
||||
/// allocator.alloc(123u64);
|
||||
///
|
||||
/// // `vec` has to grow beyond it's initial capacity
|
||||
/// vec.extend([1, 2, 3, 4]);
|
||||
///
|
||||
/// // `vec` takes up 32 bytes, and `123u64` takes up 8 bytes = 40 total.
|
||||
/// // But there's an additional 16 bytes consumed for `vec`'s original capacity of 2,
|
||||
/// // which is still using up space
|
||||
/// assert_eq!(allocator.used_bytes(), 56);
|
||||
/// ```
|
||||
///
|
||||
/// [`capacity`]: Allocator::capacity
|
||||
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.bump.iter_allocated_chunks_raw() };
|
||||
for (_, size) in chunks_iter {
|
||||
bytes += size;
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Get inner [`bumpalo::Bump`].
|
||||
///
|
||||
/// This method is not public. We don't want to expose `bumpalo::Allocator` to user.
|
||||
/// The fact that we're using `bumpalo` is an internal implementation detail.
|
||||
//
|
||||
// `#[inline(always)]` because it's a no-op
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub(crate) fn bump(&self) -> &Bump {
|
||||
&self.bump
|
||||
}
|
||||
}
|
||||
|
||||
/// SAFETY: Not actually safe, but for enabling `Send` for downstream crates.
|
||||
unsafe impl Send for Allocator {}
|
||||
/// SAFETY: Not actually safe, but for enabling `Sync` for downstream crates.
|
||||
unsafe impl Sync for Allocator {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::Allocator;
|
||||
|
||||
#[test]
|
||||
fn test_api() {
|
||||
let mut allocator = Allocator::default();
|
||||
{
|
||||
let array = allocator.alloc([123; 10]);
|
||||
assert_eq!(array, &[123; 10]);
|
||||
let str = allocator.alloc_str("hello");
|
||||
assert_eq!(str, "hello");
|
||||
}
|
||||
allocator.reset();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue