Repository: oxc-project/oxc
Pull request #10883: refactor(allocator/vec2): move `len` field from `Vec` to `RawVec`
Author: login: Dunqing
Labels: name: C-cleanup
description: Category -  technical debt or refactoring. Solution not expected to change behavior
name: A-ast-tools
description: Area - AST tools
Created: 2025-05-08T07:58:33Z | Merged: 2025-05-15T21:57:26Z

Prepare for #9706.

Move `len` field from `Vec` to `RawVec` to adapt upcoming change that reduces `len` and `cap` fields from `usize` to `u32` so that the `Vec` size can reduce from `32` to `24` without padding.

Marked `Vec` as `#[repr(transparent)]` and `RawVec` as `#[repr(C)]` to easier to solve raw transfer test failures because the fields are no longer reordered, we can statically determine the offsets, as mentioned in that comment.



Diff:
diff --git a/crates/oxc_allocator/src/vec2/mod.rs b/crates/oxc_allocator/src/vec2/mod.rs
index 254c9291a549a..a91e704757906 100644
--- a/crates/oxc_allocator/src/vec2/mod.rs
+++ b/crates/oxc_allocator/src/vec2/mod.rs
@@ -541,9 +541,9 @@ macro_rules! vec {
 /// [`insert`]: struct.Vec.html#method.insert
 /// [`reserve`]: struct.Vec.html#method.reserve
 /// [owned slice]: https://doc.rust-lang.org/std/boxed/struct.Box.html
+#[repr(transparent)]
 pub struct Vec<'bump, T: 'bump> {
     buf: RawVec<'bump, T>,
-    len: usize,
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -566,7 +566,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// ```
     #[inline]
     pub fn new_in(bump: &'bump Bump) -> Vec<'bump, T> {
-        Vec { buf: RawVec::new_in(bump), len: 0 }
+        Vec { buf: RawVec::new_in(bump) }
     }
 
     /// Constructs a new, empty `Vec<'bump, T>` with the specified capacity.
@@ -603,7 +603,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// ```
     #[inline]
     pub fn with_capacity_in(capacity: usize, bump: &'bump Bump) -> Vec<'bump, T> {
-        Vec { buf: RawVec::with_capacity_in(capacity, bump), len: 0 }
+        Vec { buf: RawVec::with_capacity_in(capacity, bump) }
     }
 
     /// Construct a new `Vec` from the given iterator's items.
@@ -687,7 +687,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
         capacity: usize,
         bump: &'bump Bump,
     ) -> Vec<'bump, T> {
-        Vec { buf: RawVec::from_raw_parts_in(ptr, capacity, bump), len: length }
+        Vec { buf: RawVec::from_raw_parts_in(ptr, bump, capacity, length) }
     }
 
     /// Returns a shared reference to the allocator backing this `Vec`.
@@ -748,7 +748,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// assert!(vec.capacity() >= 11);
     /// ```
     pub fn reserve(&mut self, additional: usize) {
-        self.buf.reserve(self.len, additional);
+        self.buf.reserve(self.buf.len, additional);
     }
 
     /// Reserves the minimum capacity for exactly `additional` more elements to
@@ -775,7 +775,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// assert!(vec.capacity() >= 11);
     /// ```
     pub fn reserve_exact(&mut self, additional: usize) {
-        self.buf.reserve_exact(self.len, additional);
+        self.buf.reserve_exact(self.buf.len, additional);
     }
 
     /// Attempts to reserve capacity for at least `additional` more elements to be inserted
@@ -799,7 +799,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// assert!(vec.capacity() >= 11);
     /// ```
     pub fn try_reserve(&mut self, additional: usize) -> Result<(), CollectionAllocErr> {
-        self.buf.try_reserve(self.len, additional)
+        self.buf.try_reserve(self.buf.len, additional)
     }
 
     /// Attempts to reserve the minimum capacity for exactly `additional` more elements to
@@ -826,7 +826,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// assert!(vec.capacity() >= 11);
     /// ```
     pub fn try_reserve_exact(&mut self, additional: usize) -> Result<(), CollectionAllocErr> {
-        self.buf.try_reserve_exact(self.len, additional)
+        self.buf.try_reserve_exact(self.buf.len, additional)
     }
 
     /// Shrinks the capacity of the vector as much as possible.
@@ -848,8 +848,8 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// assert!(vec.capacity() >= 3);
     /// ```
     pub fn shrink_to_fit(&mut self) {
-        if self.capacity() != self.len {
-            self.buf.shrink_to_fit(self.len);
+        if self.capacity() != self.buf.len {
+            self.buf.shrink_to_fit(self.buf.len);
         }
     }
 
@@ -955,14 +955,14 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// [`clear`]: #method.clear
     /// [`drain`]: #method.drain
     pub fn truncate(&mut self, len: usize) {
-        let current_len = self.len;
+        let current_len = self.buf.len;
         unsafe {
-            let mut ptr = self.as_mut_ptr().add(self.len);
+            let mut ptr = self.as_mut_ptr().add(current_len);
             // Set the final length at the end, keeping in mind that
             // dropping an element might panic. Works around a missed
             // optimization, as seen in the following issue:
             // https://github.com/rust-lang/rust/issues/51802
-            let mut local_len = SetLenOnDrop::new(&mut self.len);
+            let mut local_len = SetLenOnDrop::new(&mut self.buf.len);
 
             // drop any extra elements
             for _ in len..current_len {
@@ -1170,7 +1170,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// ```
     #[inline]
     pub unsafe fn set_len(&mut self, new_len: usize) {
-        self.len = new_len;
+        self.buf.len = new_len;
     }
 
     /// Removes an element from the vector and returns it.
@@ -1205,8 +1205,8 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
             // bounds check on hole succeeds there must be a last element (which
             // can be self[index] itself).
             let hole: *mut T = &mut self[index];
-            let last = ptr::read(self.get_unchecked(self.len - 1));
-            self.len -= 1;
+            let last = ptr::read(self.get_unchecked(self.buf.len - 1));
+            self.buf.len -= 1;
             ptr::replace(hole, last)
         }
     }
@@ -1564,13 +1564,13 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     pub fn push(&mut self, value: T) {
         // This will panic or abort if we would allocate > isize::MAX bytes
         // or if the length increment would overflow for zero-sized types.
-        if self.len == self.buf.cap() {
+        if self.buf.len == self.buf.cap() {
             self.buf.grow_one();
         }
         unsafe {
-            let end = self.buf.ptr().add(self.len);
+            let end = self.buf.ptr().add(self.buf.len);
             ptr::write(end, value);
-            self.len += 1;
+            self.buf.len += 1;
         }
     }
 
@@ -1592,11 +1592,11 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// ```
     #[inline]
     pub fn pop(&mut self) -> Option<T> {
-        if self.len == 0 {
+        if self.buf.len == 0 {
             None
         } else {
             unsafe {
-                self.len -= 1;
+                self.buf.len -= 1;
                 Some(ptr::read(self.as_ptr().add(self.len())))
             }
         }
@@ -1636,7 +1636,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
         self.reserve(count);
         let len = self.len();
         ptr::copy_nonoverlapping(other as *const T, self.as_mut_ptr().add(len), count);
-        self.len += count;
+        self.buf.len += count;
     }
 
     /// Creates a draining iterator that removes the specified range in the vector
@@ -1753,7 +1753,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// ```
     #[inline]
     pub fn len(&self) -> usize {
-        self.len
+        self.buf.len
     }
 
     /// Returns `true` if the vector contains no elements.
@@ -1802,7 +1802,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     pub fn split_off(&mut self, at: usize) -> Self {
         assert!(at <= self.len(), "`at` out of bounds");
 
-        let other_len = self.len - at;
+        let other_len = self.buf.len - at;
         let mut other = Vec::with_capacity_in(other_len, self.buf.bump());
 
         // Unsafely `set_len` and copy items to `other`.
@@ -2069,7 +2069,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
             // Use SetLenOnDrop to work around bug where compiler
             // may not realize the store through `ptr` through self.set_len()
             // don't alias.
-            let mut local_len = SetLenOnDrop::new(&mut self.len);
+            let mut local_len = SetLenOnDrop::new(&mut self.buf.len);
 
             // Write all elements except the last one
             for _ in 1..n {
@@ -2209,7 +2209,7 @@ impl<'bump, T: 'bump> ops::Deref for Vec<'bump, T> {
         unsafe {
             let p = self.buf.ptr();
             // assume(!p.is_null());
-            slice::from_raw_parts(p, self.len)
+            slice::from_raw_parts(p, self.buf.len)
         }
     }
 }
@@ -2219,7 +2219,7 @@ impl<'bump, T: 'bump> ops::DerefMut for Vec<'bump, T> {
         unsafe {
             let ptr = self.buf.ptr();
             // assume(!ptr.is_null());
-            slice::from_raw_parts_mut(ptr, self.len)
+            slice::from_raw_parts_mut(ptr, self.buf.len)
         }
     }
 }
@@ -2802,7 +2802,7 @@ impl<'a, 'bump, T> Drain<'a, 'bump, T> {
     /// Return whether we filled the entire range. (`replace_with.next()` didn’t return `None`.)
     unsafe fn fill<I: Iterator<Item = T>>(&mut self, replace_with: &mut I) -> bool {
         let vec = self.vec.as_mut();
-        let range_start = vec.len;
+        let range_start = vec.buf.len;
         let range_end = self.tail_start;
         let range_slice =
             slice::from_raw_parts_mut(vec.as_mut_ptr().add(range_start), range_end - range_start);
@@ -2810,7 +2810,7 @@ impl<'a, 'bump, T> Drain<'a, 'bump, T> {
         for place in range_slice {
             if let Some(new_item) = replace_with.next() {
                 ptr::write(place, new_item);
-                vec.len += 1;
+                vec.buf.len += 1;
             } else {
                 return false;
             }
diff --git a/crates/oxc_allocator/src/vec2/raw_vec.rs b/crates/oxc_allocator/src/vec2/raw_vec.rs
index 871c8e5bd6660..c0006660aad6a 100644
--- a/crates/oxc_allocator/src/vec2/raw_vec.rs
+++ b/crates/oxc_allocator/src/vec2/raw_vec.rs
@@ -63,10 +63,12 @@ use bumpalo::collections::CollectionAllocErr::{self, AllocErr, CapacityOverflow}
 /// field. This allows zero-sized types to not be special-cased by consumers of
 /// this type.
 #[allow(missing_debug_implementations)]
+#[repr(C)]
 pub struct RawVec<'a, T> {
     ptr: NonNull<T>,
-    cap: usize,
     a: &'a Bump,
+    cap: usize,
+    pub(super) len: usize,
 }
 
 impl<'a, T> RawVec<'a, T> {
@@ -74,7 +76,7 @@ impl<'a, T> RawVec<'a, T> {
     /// the returned RawVec.
     pub fn new_in(a: &'a Bump) -> Self {
         // `cap: 0` means "unallocated". zero-sized types are ignored.
-        RawVec { ptr: NonNull::dangling(), cap: 0, a }
+        RawVec { ptr: NonNull::dangling(), a, cap: 0, len: 0 }
     }
 
     /// Like `with_capacity` but parameterized over the choice of
@@ -111,7 +113,7 @@ impl<'a, T> RawVec<'a, T> {
                 }
             };
 
-            RawVec { ptr, cap, a }
+            RawVec { ptr, a, cap, len: 0 }
         }
     }
 }
@@ -124,8 +126,8 @@ impl<'a, T> RawVec<'a, T> {
     /// The ptr must be allocated (via the given allocator `a`), and with the given capacity. The
     /// capacity cannot exceed `isize::MAX` (only a concern on 32-bit systems).
     /// If the ptr and capacity come from a RawVec created via `a`, then this is guaranteed.
-    pub unsafe fn from_raw_parts_in(ptr: *mut T, cap: usize, a: &'a Bump) -> Self {
-        RawVec { ptr: NonNull::new_unchecked(ptr), cap, a }
+    pub unsafe fn from_raw_parts_in(ptr: *mut T, a: &'a Bump, cap: usize, len: usize) -> Self {
+        RawVec { ptr: NonNull::new_unchecked(ptr), a, cap, len }
     }
 }
 
diff --git a/tasks/ast_tools/src/generators/raw_transfer.rs b/tasks/ast_tools/src/generators/raw_transfer.rs
index 75975be5221f4..118738d822d3e 100644
--- a/tasks/ast_tools/src/generators/raw_transfer.rs
+++ b/tasks/ast_tools/src/generators/raw_transfer.rs
@@ -25,8 +25,7 @@ use crate::{
 use super::define_generator;
 
 // Offsets of `Vec`'s fields.
-// These are based on observation, and are not stable.
-// They will be fully reliable once we implement our own `Vec` type and make it `#[repr(C)]`.
+// `Vec` is `#[repr(transparent)]` and `RawVec` is `#[repr(C)]`, so these offsets are fixed.
 const VEC_PTR_FIELD_OFFSET: usize = 0;
 const VEC_LEN_FIELD_OFFSET: usize = 24;
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the memory layout of the allocator’s Vec and RawVec types in preparation for reducing field sizes. The changes include updating field offsets in the raw transfer generator, moving the len field from Vec to RawVec with corresponding constructor updates, and adjusting Vec methods to reference the new buf.len field.

### Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

| File                                                         | Description                                                   |
| ------------------------------------------------------------ | ------------------------------------------------------------- |
| tasks/ast_tools/src/generators/raw_transfer.rs              | Updated the Vec field offset constant and comment to reflect the new layout. |
| crates/oxc_allocator/src/vec2/raw_vec.rs                      | Added the len field to RawVec and updated its constructors.   |
| crates/oxc_allocator/src/vec2/mod.rs                           | Removed the len field from Vec and modified methods to use buf.len. |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**tasks/ast_tools/src/generators/raw_transfer.rs:30**
* Ensure that the new VEC_LEN_FIELD_OFFSET value is valid on all targeted platforms given the revised memory layout of Vec and RawVec; adding a clarifying comment about this assumption might be helpful.
```
const VEC_LEN_FIELD_OFFSET: usize = 8;
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T07:22:07Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T07:22:47Z
user: login: Dunqing
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T07:37:57Z
user: login: overlookmotel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T09:45:23Z
user: login: overlookmotel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T19:20:37Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T19:21:57Z
user: login: overlookmotel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T19:24:03Z
user: login: overlookmotel
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T21:48:41Z

Review comments:
diff_hunk: @@ -124,8 +126,8 @@ impl<'a, T> RawVec<'a, T> {
     /// The ptr must be allocated (via the given allocator `a`), and with the given capacity. The
     /// capacity cannot exceed `isize::MAX` (only a concern on 32-bit systems).
     /// If the ptr and capacity come from a RawVec created via `a`, then this is guaranteed.
-    pub unsafe fn from_raw_parts_in(ptr: *mut T, cap: usize, a: &'a Bump) -> Self {
-        RawVec { ptr: NonNull::new_unchecked(ptr), cap, a }
+    pub unsafe fn from_raw_parts_in(ptr: *mut T, a: &'a Bump, cap: usize, len: usize) -> Self {
path: crates/oxc_allocator/src/vec2/raw_vec.rs
user: login: Copilot
body: The parameter order in the from_raw_parts_in function has changed. Consider adding documentation to clarify the new order and ensure that future users do not inadvertently misuse the parameters.
created_at: 2025-05-14T07:22:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -955,14 +955,14 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// [`clear`]: #method.clear
     /// [`drain`]: #method.drain
     pub fn truncate(&mut self, len: usize) {
-        let current_len = self.len;
+        let current_len = self.buf.len;
         unsafe {
-            let mut ptr = self.as_mut_ptr().add(self.len);
+            let mut ptr = self.as_mut_ptr().add(current_len);
path: crates/oxc_allocator/src/vec2/mod.rs
user: login: graphite-app[bot]
body: There appears to be a logic error in the `truncate` method. The pointer calculation `self.as_mut_ptr().add(current_len)` is incorrect - it's adding the full current length to the pointer, but we should be starting from the target length (`len` parameter) since we're dropping elements from `len` to `current_len`.

The line should be:
```rust
let mut ptr = self.as_mut_ptr().add(len);
```

This ensures we're correctly positioning the pointer at the first element that needs to be dropped, rather than at the end of the collection.
```suggestion
            let mut ptr = self.as_mut_ptr().add(len);
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-14T07:22:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -955,14 +955,14 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
     /// [`clear`]: #method.clear
     /// [`drain`]: #method.drain
     pub fn truncate(&mut self, len: usize) {
-        let current_len = self.len;
+        let current_len = self.buf.len;
         unsafe {
-            let mut ptr = self.as_mut_ptr().add(self.len);
+            let mut ptr = self.as_mut_ptr().add(current_len);
path: crates/oxc_allocator/src/vec2/mod.rs
user: login: Dunqing
body: Wrong!
created_at: 2025-05-14T07:37:56Z
author_association: MEMBER
diff_hunk: @@ -25,10 +25,9 @@ use crate::{
 use super::define_generator;
 
 // Offsets of `Vec`'s fields.
-// These are based on observation, and are not stable.
-// They will be fully reliable once we implement our own `Vec` type and make it `#[repr(C)]`.
+// The `Vec` has marked `#[repr(C)]` layout, so the offsets are fixed.
 const VEC_PTR_FIELD_OFFSET: usize = 0;
-const VEC_LEN_FIELD_OFFSET: usize = 24;
+const VEC_LEN_FIELD_OFFSET: usize = 8;
path: tasks/ast_tools/src/generators/raw_transfer.rs
user: login: overlookmotel
body: This needs to go back to 24. Test NAPI CI task has not failed, but it's still running. Probably it's in an infinite loop.
created_at: 2025-05-14T09:45:23Z
author_association: MEMBER
diff_hunk: @@ -25,10 +25,9 @@ use crate::{
 use super::define_generator;
 
 // Offsets of `Vec`'s fields.
-// These are based on observation, and are not stable.
-// They will be fully reliable once we implement our own `Vec` type and make it `#[repr(C)]`.
+// The `Vec` has marked `#[repr(C)]` layout, so the offsets are fixed.
 const VEC_PTR_FIELD_OFFSET: usize = 0;
-const VEC_LEN_FIELD_OFFSET: usize = 24;
+const VEC_LEN_FIELD_OFFSET: usize = 8;
path: tasks/ast_tools/src/generators/raw_transfer.rs
user: login: overlookmotel
body: I've fixed this.
created_at: 2025-05-14T19:20:37Z
author_association: MEMBER
diff_hunk: @@ -25,8 +25,7 @@ use crate::{
 use super::define_generator;
 
 // Offsets of `Vec`'s fields.
-// These are based on observation, and are not stable.
-// They will be fully reliable once we implement our own `Vec` type and make it `#[repr(C)]`.
+// `Vec` is `#[repr(transparent)]` and `RawVec` is `#[repr(C)]`, so these offsets are fixed.
 const VEC_PTR_FIELD_OFFSET: usize = 0;
 const VEC_LEN_FIELD_OFFSET: usize = 24;
path: tasks/ast_tools/src/generators/raw_transfer.rs
user: login: graphite-app[bot]
body: The `VEC_LEN_FIELD_OFFSET` constant needs to be updated to reflect the new structure layout. Since `len` has been moved from `Vec` to `RawVec` and `Vec` is now `#[repr(transparent)]` over `RawVec`, the offset of 24 is no longer correct. The offset should be recalculated based on the new field position within the `RawVec` struct, which is now at offset 16 (after `ptr` and `a` fields).
```suggestion
 const VEC_LEN_FIELD_OFFSET: usize = 16;
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-14T19:21:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,8 +25,7 @@ use crate::{
 use super::define_generator;
 
 // Offsets of `Vec`'s fields.
-// These are based on observation, and are not stable.
-// They will be fully reliable once we implement our own `Vec` type and make it `#[repr(C)]`.
+// `Vec` is `#[repr(transparent)]` and `RawVec` is `#[repr(C)]`, so these offsets are fixed.
 const VEC_PTR_FIELD_OFFSET: usize = 0;
 const VEC_LEN_FIELD_OFFSET: usize = 24;
path: tasks/ast_tools/src/generators/raw_transfer.rs
user: login: overlookmotel
body: Completely, and bizarrely, wrong! `len` is after `cap` field, so at offset 24.
created_at: 2025-05-14T19:24:03Z
author_association: MEMBER

PR comments:
user: login: Dunqing
created_at: 2025-05-08T07:58:48Z
body: * **#11050** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11050?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#10884** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/10884?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#10883** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/10883?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/10883?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `main`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>0-merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/oxc-project?ref=merge-queue-instructions-in-stack-comment&prId=10883).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER
user: login: codspeed-hq[bot]
created_at: 2025-05-08T08:32:53Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Instrumentation Performance Report](https://codspeed.io/oxc-project/oxc/branches/05-08-refactor_allocator_vec2_move_rawvec_fields_to_vec)

### Merging #10883 will **not alter performance**

<sub>Comparing <code>05-08-refactor_allocator_vec2_move_rawvec_fields_to_vec</code> (7d54577) with <code>main</code> (b9e51e2)</sub>



### Summary

`✅ 36` untouched benchmarks  




author_association: NONE
user: login: overlookmotel
created_at: 2025-05-08T17:32:18Z
body: *Now* I understand what you're doing.

However, it's rather complicated!

I expect the perf regression is coming from the `&mut` references in `RawVec`. You'd expect that wouldn't cost anything if `RawVec`'s methods were inlined into `Vec`'s methods, but they're probably not always (and you probably don't want them always to be).

It's tricky, because we ideally want to maintain the separation between `Vec` and `RawVec`, but we can't when `len` and `capacity` fields are `u32`, without ending up with padding bytes in both `RawVec` and `Vec` - so `Vec` remains 32 bytes.

I can see 3 alternative approaches:

1. Merge `Vec` and `RawVec` by moving all `RawVec`'s fields and methods into `Vec`.
2. Move `len` field into `RawVec`.
3. Use `#[repr]` attribute hacks to avoid having to move anything.

### `#[repr]` approach

This is what that option looks like:

```rs
#[repr(C)]
struct Vec<'a> {
    _align: [usize; 0],
    buf: RawVec<'a>,
    len: u32,
}

#[repr(C, packed)]
struct RawVec<'a> {
    ptr: NonNull<u8>,
    allocator: &'a Allocator,
    capacity: u32,
}

#[cfg(target_pointer_width = "64")]
const _: () = {
    use std::mem::offset_of;

    assert!(size_of::<RawVec>() == 20); // Not 24 due to `#[repr(packed)]`
    assert!(align_of::<RawVec>() == 1); // Due to `#[repr(packed)]`
    assert!(offset_of!(RawVec, ptr) == 0);
    assert!(offset_of!(RawVec, allocator) == 8);
    assert!(offset_of!(RawVec, capacity) == 16);

    assert!(size_of::<Vec>() == 24);
    assert!(align_of::<Vec>() == 8); // Due to `_align: [usize; 0]`
    assert!(offset_of!(Vec, buf) == 0);
    assert!(offset_of!(Vec, len) == 20);
};
```

* `#[repr(packed)]` makes `RawVec` act like a `[u8; 20]`, aligned on 1. So it doesn't contain any padding bytes.
* `RawVec` and `len: u32` can therefore be combined together without any padding bytes (like if it was `([u8; 20], u32)`).
* `#[repr(C)]` on both `Vec` and `RawVec` "locks" the field orders to what we've specified.
* `_align: [usize; 0]` in `Vec` is a zero-sized type which ensures `Vec` is pointer aligned i.e. aligned on 8 on 64-bit systems.

Because we've carefully made sure every field is laid out where we want it, and the whole of `Vec` is aligned on 8, that ensures all the fields are correctly aligned for their types.

```
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<------------------------ RawVec -------------------------> <-- len -->
<-------- ptr --------> <----- allocator -----> <-- cap -->
```

Note: `capacity` field has to be last in `RawVec`. If field order was `ptr`, `capacity`, `allocator`, then `allocator` field would be out of alignment. Ditto `len` field must be last in `Vec`.

I think with these `#[repr]` attributes we don't have to move any methods or fields between `Vec` and `RawVec`.

The downside is it's hacky and dangerous! We MUST NOT perform any operations on a `RawVec` unless it's wrapped in a `Vec`, or it's instant undefined behavior, because the fields will be out of alignment.

### What to do?

If our aim at this point is to get a fairly accurate idea of what perf gain is from switching to `u32`, maybe it's fine to take this hacky route for now.

If we do go ahead with `u32` (which I hope we will), then we could later on switch to one of the other less hacky options.

What do you think?
author_association: MEMBER
user: login: overlookmotel
created_at: 2025-05-08T17:50:56Z
body: On second thoughts, forget everything I've said about `#[repr(packed)]` etc!

Probably just moving `len` field into `RawVec` is the simplest option.

If `len` is `pub(super)`, it can still be accessed from `Vec`, so very little other code would need to change, and we avoid potential UB and `#[repr]` hackery. We also avoid the `&mut` references in the implementation in this PR.

author_association: MEMBER
user: login: Boshen
created_at: 2025-05-09T01:17:53Z
body: We should add a `assert_eq!(std::mem::size_of::<T>(), 32);` to these types.
author_association: MEMBER
user: login: Dunqing
created_at: 2025-05-14T07:39:26Z
body: > We should add a `assert_eq!(std::mem::size_of::<T>(), 32);` to these types.

All of `generated/assert_layout.rs` is already included this assert actually.
author_association: MEMBER
user: login: overlookmotel
created_at: 2025-05-14T08:49:14Z
body: There should be no perf impact from moving `len` field into `RawVec` in itself. I assume the perf difference is that field order changes in the process.

But to test that assumption (and make sure we're not getting false results from CodSpeed on either/both of these PRs), I suggest re-ordering the fields of `RawVec` in this PR as:

```rs
#[repr(C)]
pub struct RawVec<'a, T> {
    ptr: NonNull<T>,
    a: &'a Bump,
    cap: usize,
    pub(super) len: usize,
}
```

This is the same order the fields were in before this PR (with default `#[repr(Rust)]`, compiler moves fields with niches e.g. `&T` to be before fields without niches e.g. `usize`). I checked that's correct by adding this code (on main):

```rs
const _: () = {
    use std::mem::offset_of;
    assert!(size_of::<Vec<u8>>() == 32);
    assert!(align_of::<Vec<u8>>() == 8);
    assert!(offset_of!(Vec<u8>, buf) == 0);
    assert!(offset_of!(Vec<u8>, len) == 24);
};

const _: () = {
    use std::mem::offset_of;
    assert!(size_of::<RawVec<u8>>() == 24);
    assert!(align_of::<RawVec<u8>>() == 8);
    assert!(offset_of!(RawVec<u8>, ptr) == 0);
    assert!(offset_of!(RawVec<u8>, a) == 8);
    assert!(offset_of!(RawVec<u8>, cap) == 16);
};
```

Those assertions all pass on main branch. If you re-arrange `RawVec`'s fields as suggested above, they should still pass on this PR.

After that change, this PR should have zero perf impact. If it still does, something is wrong.

author_association: MEMBER
user: login: overlookmotel
created_at: 2025-05-14T19:22:28Z
body: CI got stuck (as usual!). I've rebased on main, fixed the raw transfer test failure, and force pushed (ditto for #10884).
author_association: MEMBER
user: login: overlookmotel
created_at: 2025-05-15T21:48:57Z
body: ### Merge activity

* **May 15, 5:48 PM EDT**: The merge label '0-merge' was detected. This PR will be added to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc) once it meets the requirements.
* **May 15, 5:49 PM EDT**: `overlookmotel` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).
* **May 15, 5:57 PM EDT**: Merged by the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).

author_association: MEMBER