diff --git a/Cargo.lock b/Cargo.lock index 160f18725bee4..f99270f301f2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1572,6 +1572,7 @@ name = "oxc_allocator" version = "0.53.0" dependencies = [ "allocator-api2", + "assert-unchecked", "bumpalo", "hashbrown 0.15.2", "oxc_estree", diff --git a/crates/oxc_allocator/Cargo.toml b/crates/oxc_allocator/Cargo.toml index 3809dbf983578..72b1390cd1894 100644 --- a/crates/oxc_allocator/Cargo.toml +++ b/crates/oxc_allocator/Cargo.toml @@ -22,6 +22,7 @@ doctest = false oxc_estree = { workspace = true, optional = true } allocator-api2 = { workspace = true } +assert-unchecked = { workspace = true } bumpalo = { workspace = true, features = ["allocator-api2", "collections"] } hashbrown = { workspace = true, default-features = false, features = ["inline-more", "allocator-api2"] } rustc-hash = { workspace = true } diff --git a/crates/oxc_allocator/src/vec.rs b/crates/oxc_allocator/src/vec.rs index 14d172bfbf1ae..ebb761747fefb 100644 --- a/crates/oxc_allocator/src/vec.rs +++ b/crates/oxc_allocator/src/vec.rs @@ -16,6 +16,7 @@ use std::{ }; use allocator_api2::vec::Vec as InnerVec; +use assert_unchecked::assert_unchecked; use bumpalo::Bump; #[cfg(any(feature = "serialize", test))] use oxc_estree::{ESTree, Serializer as ESTreeSerializer}; @@ -201,6 +202,54 @@ impl<'alloc, T> Vec<'alloc, T> { // `ptr` was created from a `&mut [T]`. unsafe { Box::from_non_null(ptr) } } + + /// Appends an element to the end of the [`Vec`]. + /// + /// # Panics + /// Panics if the new capacity exceeds `isize::MAX` bytes. + /// + /// # Example + /// ``` + /// # use oxc_allocator::{Allocator, Vec}; + /// # let allocator = Allocator::new(); + /// let mut vec = Vec::new_in(&allocator); + /// vec.push(1); + /// vec.push(2); + /// vec.push(3); + /// assert_eq!(vec, [1, 2, 3]); + /// ``` + // + // Override `allocator_api2::vec::Vec`'s `push` method, because it's inefficient. + // + // Its fast path is for the case when the `Vec` is full to capacity, and needs to grow. + // But growth strategy is doubling, so it's far more common that there is already sufficient + // capacity. So that should be the fast path. + // + // Achieve this by delegating the slow path to `push_slow`, which is marked `#[cold]` + // and `#[inline(never)]`. Both branches just call `push` on the inner `Vec`, but this arrangement + // guides the compiler to optimize `push` for the common case. + #[inline(always)] + pub fn push(&mut self, value: T) { + if self.len() == self.capacity() { + // SAFETY: Vector is full to capacity + unsafe { self.push_slow(value) }; + } else { + self.0.push(value); + } + } + + /// Push when vector is already full to capacity. + /// i.e. `self.len() == self.capacity()`. + /// + /// # SAFETY + /// Vector must be full to capacity. + #[cold] + #[inline(never)] + unsafe fn push_slow(&mut self, value: T) { + // SAFETY: Caller guarantees vector is full to capacity + unsafe { assert_unchecked!(self.len() == self.capacity()) }; + self.0.push(value); + } } impl<'alloc, T> ops::Deref for Vec<'alloc, T> {