mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(allocator): per-platform Arena::new_fixed_size implementations (#22088)
Introduce separate `Arena::new_fixed_size` implementations for different platforms. Different platforms have differing abilities to service allocation requests with high alignment, so a different allocation strategy is appropriate for each: - Mac OS: System allocator refuses 4 GiB-aligned allocation requests. Allocate ~4 GiB with 2 GiB alignment, then use either the top or bottom half for the arena chunk. - Windows: System allocator cannot service allocations with any alignment over 16. Allocate ~6 GiB with 16 alignment, and then find a ~2 GiB block within that allocation which is aligned on 4 GiB. This is what `std` was doing already, but implementing it ourself sidesteps an annoyance with `std`'s implementation. - Linux: System allocator can service high-alignment requests, so just ask for exactly what we want (~2 GiB with 4 GiB alignment). This reduces virtual memory usage on Linux, side-steps `std`'s workaround on Windows, and is unchanged for Mac OS. This also prepares the way for using `VirtualAlloc`\-based allocation on Windows, to avoid committing large amounts of memory - the cause of OOM problems in Oxlint with JS plugins on Windows (#19395).
This commit is contained in:
1 parent
94040ce45b
commit
0bf0cb938e
4 files changed
+204
-37
No files matched your search
@@ -0,0 +1,47 @@
|
||||
use std::{
|
||||
alloc::{GlobalAlloc, Layout, System},
|
||||
ptr::NonNull,
|
||||
};
|
||||
|
||||
use crate::generated::fixed_size_constants::{BLOCK_ALIGN, BLOCK_SIZE};
|
||||
|
||||
use super::super::Arena;
|
||||
|
||||
// Linux's system allocator supports high alignment allocations directly, so we just request what we want.
|
||||
// We assume that other non-MacOS, non-Windows platforms also support high alignment allocations.
|
||||
|
||||
/// Layout of backing allocations.
|
||||
const ALLOC_LAYOUT: Layout = match Layout::from_size_align(BLOCK_SIZE, BLOCK_ALIGN) {
|
||||
Ok(layout) => layout,
|
||||
Err(_) => unreachable!(),
|
||||
};
|
||||
|
||||
const _: () = assert!(ALLOC_LAYOUT.size() > 0);
|
||||
|
||||
impl<const MIN_ALIGN: usize> Arena<MIN_ALIGN> {
|
||||
/// Construct a static-sized [`Arena`] backed by an allocation made via the [`System`] allocator.
|
||||
///
|
||||
/// The returned [`Arena`] uses a single chunk of `BLOCK_SIZE` bytes, aligned on `BLOCK_ALIGN`.
|
||||
/// It cannot grow.
|
||||
///
|
||||
/// Returns `None` if the allocation fails.
|
||||
///
|
||||
/// See module-level docs for the rationale and platform-specific allocation strategy.
|
||||
pub fn new_fixed_size() -> Option<Self> {
|
||||
// Allocate block of memory aligned on `BLOCK_ALIGN`.
|
||||
// SAFETY: `ALLOC_LAYOUT` does not have zero size.
|
||||
let alloc_ptr = unsafe { System.alloc(ALLOC_LAYOUT) };
|
||||
let alloc_ptr = NonNull::new(alloc_ptr)?;
|
||||
|
||||
debug_assert!(alloc_ptr.addr().get().is_multiple_of(BLOCK_ALIGN));
|
||||
|
||||
// SAFETY:
|
||||
// * Region starting at `alloc_ptr` with `BLOCK_SIZE` bytes is the allocation we just made.
|
||||
// * `alloc_ptr` has high alignment (`BLOCK_ALIGN`).
|
||||
// * `BLOCK_SIZE` is large and a multiple of 16.
|
||||
// * `alloc_ptr` has permission for writes.
|
||||
let arena = unsafe { Self::from_raw_parts(alloc_ptr, BLOCK_SIZE, alloc_ptr, ALLOC_LAYOUT) };
|
||||
|
||||
Some(arena)
|
||||
}
|
||||
}
|
||||
+20
-37
@@ -1,5 +1,3 @@
|
||||
//! [`Arena::new_fixed_size`]
|
||||
|
||||
use std::{
|
||||
alloc::{GlobalAlloc, Layout, System},
|
||||
ptr::NonNull,
|
||||
@@ -7,43 +5,33 @@ use std::{
|
||||
|
||||
use crate::generated::fixed_size_constants::{BLOCK_ALIGN, BLOCK_SIZE};
|
||||
|
||||
use super::{Arena, CHUNK_ALIGN, CHUNK_FOOTER_SIZE};
|
||||
use super::super::Arena;
|
||||
|
||||
// System allocator on Mac OS refuses allocations with 4 GiB alignment, so we over-allocate
|
||||
// `BLOCK_SIZE + TWO_GIB` (4 GiB - 16) bytes with 2 GiB alignment, and then use either the 1st or 2nd half
|
||||
// of the allocation, one of which is guaranteed to be on a 4 GiB boundary.
|
||||
// <https://github.com/rust-lang/rust/blob/556d20a834126d2d0ac20743b9792b8474d6d03c/library/std/src/sys/alloc/unix.rs#L16-L27>
|
||||
// <https://github.com/rust-lang/rust/issues/30170>
|
||||
|
||||
const TWO_GIB: usize = 1 << 31;
|
||||
const FOUR_GIB: usize = 1 << 32;
|
||||
|
||||
// What we ideally want is an allocation 2 GiB in size, aligned on 4 GiB.
|
||||
// But system allocator on Mac OS refuses allocations with 4 GiB alignment.
|
||||
// https://github.com/rust-lang/rust/blob/556d20a834126d2d0ac20743b9792b8474d6d03c/library/std/src/sys/alloc/unix.rs#L16-L27
|
||||
// https://github.com/rust-lang/rust/issues/30170
|
||||
//
|
||||
// So we instead allocate 4 GiB with 2 GiB alignment, and then use either the 1st or 2nd half
|
||||
// of the allocation, one of which is guaranteed to be on a 4 GiB boundary.
|
||||
//
|
||||
// TODO: We could use this workaround only on Mac OS, and just allocate what we actually want on Linux.
|
||||
// Windows OS allocator also doesn't support high alignment allocations, so Rust contains a workaround
|
||||
// which over-allocates (6 GiB in this case).
|
||||
// https://github.com/rust-lang/rust/blob/556d20a834126d2d0ac20743b9792b8474d6d03c/library/std/src/sys/alloc/windows.rs#L120-L137
|
||||
// Could just use that built-in workaround, rather than implementing our own, or allocate a 6 GiB chunk
|
||||
// with alignment 16, to skip Rust's built-in workaround.
|
||||
// Note: Rust's workaround will likely commit a whole page of memory, just to store the real pointer.
|
||||
const ALLOC_SIZE: usize = BLOCK_SIZE + TWO_GIB;
|
||||
const ALLOC_ALIGN: usize = TWO_GIB;
|
||||
|
||||
/// Layout of backing allocations for fixed-size allocators.
|
||||
const _: () = {
|
||||
// The over-alloc trick relies on the chunk fitting in either half of the allocation
|
||||
assert!(BLOCK_SIZE <= TWO_GIB);
|
||||
assert!(BLOCK_ALIGN == FOUR_GIB);
|
||||
};
|
||||
|
||||
/// Layout of backing allocations.
|
||||
const ALLOC_LAYOUT: Layout = match Layout::from_size_align(ALLOC_SIZE, ALLOC_ALIGN) {
|
||||
Ok(layout) => layout,
|
||||
Err(_) => unreachable!(),
|
||||
};
|
||||
|
||||
const _: () = {
|
||||
// `ChunkFooter` lives in the last `CHUNK_FOOTER_SIZE` bytes of the block and must be aligned on `CHUNK_ALIGN` (16).
|
||||
// `BLOCK_SIZE` and `CHUNK_FOOTER_SIZE` are both multiples of `CHUNK_ALIGN`.
|
||||
assert!(BLOCK_SIZE > 0);
|
||||
assert!(BLOCK_SIZE.is_multiple_of(CHUNK_ALIGN));
|
||||
assert!(BLOCK_SIZE >= CHUNK_FOOTER_SIZE);
|
||||
assert!(CHUNK_FOOTER_SIZE.is_multiple_of(CHUNK_ALIGN));
|
||||
};
|
||||
const _: () = assert!(ALLOC_LAYOUT.size() > 0);
|
||||
|
||||
impl<const MIN_ALIGN: usize> Arena<MIN_ALIGN> {
|
||||
/// Construct a static-sized [`Arena`] backed by an allocation made via the [`System`] allocator.
|
||||
@@ -51,12 +39,9 @@ impl<const MIN_ALIGN: usize> Arena<MIN_ALIGN> {
|
||||
/// The returned [`Arena`] uses a single chunk of `BLOCK_SIZE` bytes, aligned on `BLOCK_ALIGN`.
|
||||
/// It cannot grow.
|
||||
///
|
||||
/// Allocation is made via [`System`] allocator, bypassing any registered alternative global allocator
|
||||
/// (e.g. Mimalloc in linter). Mimalloc complains that it cannot serve allocations with high alignment,
|
||||
/// and presumably it's pointless to try to obtain such large allocations from a thread-local heap,
|
||||
/// so better to go direct to the system allocator anyway.
|
||||
///
|
||||
/// Returns `None` if the allocation fails.
|
||||
///
|
||||
/// See module-level docs for the rationale and platform-specific allocation strategy.
|
||||
pub fn new_fixed_size() -> Option<Self> {
|
||||
// Allocate block of memory.
|
||||
// SAFETY: `ALLOC_LAYOUT` does not have zero size.
|
||||
@@ -73,21 +58,19 @@ impl<const MIN_ALIGN: usize> Arena<MIN_ALIGN> {
|
||||
// Chunk occupies 2nd half of the allocation.
|
||||
//
|
||||
// Either way, `chunk_ptr` is aligned on 4 GiB.
|
||||
let offset = alloc_ptr.as_ptr() as usize % FOUR_GIB;
|
||||
let offset = alloc_ptr.addr().get() % FOUR_GIB;
|
||||
// SAFETY: We allocated 4 GiB of memory, so adding `offset` to `alloc_ptr` is in bounds
|
||||
let chunk_ptr = unsafe { alloc_ptr.add(offset) };
|
||||
|
||||
debug_assert!(chunk_ptr.addr().get().is_multiple_of(BLOCK_ALIGN));
|
||||
|
||||
// The `Arena`'s chunk fills the whole `BLOCK_SIZE` block.
|
||||
// `ChunkFooter` sits in the last `CHUNK_FOOTER_SIZE` bytes of the block.
|
||||
//
|
||||
// SAFETY:
|
||||
// * Region starting at `chunk_ptr` with `BLOCK_SIZE` bytes is within the allocation we just made.
|
||||
// * `chunk_ptr` has high alignment (4 GiB).
|
||||
// * `BLOCK_SIZE` is large and a multiple of 16.
|
||||
// * `chunk_ptr` has permission for writes.
|
||||
// * `chunk_ptr` and `alloc_ptr` have permission for writes.
|
||||
let arena = unsafe { Self::from_raw_parts(chunk_ptr, BLOCK_SIZE, alloc_ptr, ALLOC_LAYOUT) };
|
||||
|
||||
Some(arena)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//! [`Arena::new_fixed_size`]
|
||||
//!
|
||||
//! Construct an [`Arena`] of fixed size, aligned on a high boundary, backed by an allocation made via
|
||||
//! [`System`] allocator. Used by raw transfer.
|
||||
//!
|
||||
//! Allocating via [`System`] bypasses any registered alternative global allocator (e.g. Mimalloc in linter).
|
||||
//! Mimalloc complains that it cannot serve allocations with high alignment, and presumably it's pointless
|
||||
//! to try to obtain such large allocations from a thread-local heap, so better to go direct to the system
|
||||
//! allocator anyway.
|
||||
//!
|
||||
//! The implementation differs between platforms, due to differences in what platforms' system allocators support:
|
||||
//!
|
||||
//! * Mac OS: System allocator refuses allocations with 4 GiB alignment.
|
||||
//! See <https://github.com/rust-lang/rust/issues/30170>.
|
||||
//! We over-allocate `BLOCK_SIZE + TWO_GIB` (4 GiB - 16) bytes with 2 GiB alignment,
|
||||
//! then use whichever half of the allocation is aligned on `BLOCK_ALIGN`.
|
||||
//!
|
||||
//! * Windows: System allocator also doesn't support high alignment allocations, but Rust's `std` contains
|
||||
//! a workaround for servicing high-alignment requests.
|
||||
//! We side-step that by over-allocating `BLOCK_SIZE + BLOCK_ALIGN` (6 GiB - 16) bytes with alignment 16,
|
||||
//! then aligning the returned pointer to `BLOCK_ALIGN` (4 GiB) ourselves.
|
||||
//! This avoids `std`'s workaround committing a whole extra page just to store the real allocation pointer.
|
||||
//!
|
||||
//! * Linux: System allocator supports high alignment, so we just request what we want.
|
||||
//!
|
||||
//! * Other: Assume same as Linux.
|
||||
//!
|
||||
//! [`Arena`]: super::Arena
|
||||
//! [`Arena::new_fixed_size`]: super::Arena::new_fixed_size
|
||||
//! [`System`]: std::alloc::System
|
||||
|
||||
use crate::generated::fixed_size_constants::BLOCK_SIZE;
|
||||
|
||||
use super::{CHUNK_ALIGN, CHUNK_FOOTER_SIZE};
|
||||
|
||||
// `ChunkFooter` lives in the last `CHUNK_FOOTER_SIZE` bytes of the block and must be aligned on `CHUNK_ALIGN` (16).
|
||||
// `BLOCK_SIZE` and `CHUNK_FOOTER_SIZE` are both multiples of `CHUNK_ALIGN`.
|
||||
const _: () = {
|
||||
assert!(BLOCK_SIZE > 0);
|
||||
assert!(BLOCK_SIZE.is_multiple_of(CHUNK_ALIGN));
|
||||
assert!(BLOCK_SIZE >= CHUNK_FOOTER_SIZE);
|
||||
assert!(CHUNK_FOOTER_SIZE.is_multiple_of(CHUNK_ALIGN));
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
mod linux;
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::{
|
||||
alloc::{GlobalAlloc, Layout, System},
|
||||
ptr::NonNull,
|
||||
};
|
||||
|
||||
use crate::generated::fixed_size_constants::{BLOCK_ALIGN, BLOCK_SIZE};
|
||||
|
||||
use super::super::Arena;
|
||||
|
||||
// Windows system allocator doesn't support high alignment allocations directly.
|
||||
//
|
||||
// Rust's `std` implements a workaround which over-allocates `size + align` bytes, and stores the real pointer
|
||||
// in a hidden slot before the returned allocation, likely committing an extra page just for the 8-byte pointer.
|
||||
// <https://github.com/rust-lang/rust/blob/556d20a834126d2d0ac20743b9792b8474d6d03c/library/std/src/sys/alloc/windows.rs#L120-L137>
|
||||
//
|
||||
// That's unnecessary for our use case, because we already store the real allocation pointer in the `ChunkFooter`.
|
||||
// So we side-step `std`'s workaround by requesting a low-alignment allocation that's large enough to find
|
||||
// a `BLOCK_ALIGN`-aligned chunk of `BLOCK_SIZE` bytes inside it, and we align the chunk pointer to
|
||||
// `BLOCK_ALIGN` ourselves.
|
||||
|
||||
const ALLOC_SIZE: usize = BLOCK_SIZE + BLOCK_ALIGN;
|
||||
const ALLOC_ALIGN: usize = 16;
|
||||
|
||||
// For rounding up to `BLOCK_ALIGN` to make sense, `BLOCK_ALIGN` must be a multiple of `ALLOC_ALIGN`
|
||||
const _: () = assert!(BLOCK_ALIGN.is_multiple_of(ALLOC_ALIGN));
|
||||
|
||||
/// Layout of backing allocations.
|
||||
const ALLOC_LAYOUT: Layout = match Layout::from_size_align(ALLOC_SIZE, ALLOC_ALIGN) {
|
||||
Ok(layout) => layout,
|
||||
Err(_) => unreachable!(),
|
||||
};
|
||||
|
||||
const _: () = assert!(ALLOC_LAYOUT.size() > 0);
|
||||
|
||||
impl<const MIN_ALIGN: usize> Arena<MIN_ALIGN> {
|
||||
/// Construct a static-sized [`Arena`] backed by an allocation made via the [`System`] allocator.
|
||||
///
|
||||
/// The returned [`Arena`] uses a single chunk of `BLOCK_SIZE` bytes, aligned on `BLOCK_ALIGN`.
|
||||
/// It cannot grow.
|
||||
///
|
||||
/// Returns `None` if the allocation fails.
|
||||
///
|
||||
/// See module-level docs for the rationale and platform-specific allocation strategy.
|
||||
///
|
||||
/// [`System`]: std::alloc::System
|
||||
pub fn new_fixed_size() -> Option<Self> {
|
||||
// Allocate block of memory with low alignment, large enough to find a `BLOCK_ALIGN`-aligned chunk inside.
|
||||
// SAFETY: `ALLOC_LAYOUT` does not have zero size.
|
||||
let alloc_ptr = unsafe { System.alloc(ALLOC_LAYOUT) };
|
||||
let alloc_ptr = NonNull::new(alloc_ptr)?;
|
||||
|
||||
// Get pointer to use for allocator chunk, by rounding `alloc_ptr` up to the next `BLOCK_ALIGN` boundary.
|
||||
//
|
||||
// `offset` is the number of bytes between `alloc_ptr` and that next-aligned address:
|
||||
// `(BLOCK_ALIGN - (alloc_addr % BLOCK_ALIGN)) % BLOCK_ALIGN`
|
||||
//
|
||||
// The final `% BLOCK_ALIGN` is needed because if `alloc_addr` is already aligned,
|
||||
// `BLOCK_ALIGN - (alloc_addr % BLOCK_ALIGN)` = `BLOCK_ALIGN - 0` = `BLOCK_ALIGN`. But we want `0`.
|
||||
// (actually it doesn't matter, `BLOCK_ALIGN` would also be valid, but 0 is neater.)
|
||||
//
|
||||
// The expression below collapses both `%`s into a single masked unsigned negation.
|
||||
// `wrapping_neg()` computes `-alloc_addr (mod 2^N)` (where `N = usize::BITS`),
|
||||
// so taking it `mod BLOCK_ALIGN` gives `-alloc_addr (mod BLOCK_ALIGN)` which is exactly the formula above.
|
||||
// (Compiler reduces `% BLOCK_ALIGN` to `& (BLOCK_ALIGN - 1)` because `BLOCK_ALIGN` is a power-of-2 const.)
|
||||
//
|
||||
// `offset` is in `0..BLOCK_ALIGN`, so `chunk_ptr + BLOCK_SIZE` is within the allocation
|
||||
// (we allocated `BLOCK_SIZE + BLOCK_ALIGN` bytes).
|
||||
let alloc_addr = alloc_ptr.addr().get();
|
||||
let offset = alloc_addr.wrapping_neg() % BLOCK_ALIGN;
|
||||
// SAFETY: `offset < BLOCK_ALIGN` and we allocated `BLOCK_SIZE + BLOCK_ALIGN` bytes,
|
||||
// so `alloc_ptr + offset` is in bounds
|
||||
let chunk_ptr = unsafe { alloc_ptr.add(offset) };
|
||||
|
||||
debug_assert!(chunk_ptr.addr().get().is_multiple_of(BLOCK_ALIGN));
|
||||
|
||||
// SAFETY:
|
||||
// * Region starting at `chunk_ptr` with `BLOCK_SIZE` bytes is within the allocation we just made.
|
||||
// * `chunk_ptr` has high alignment (`BLOCK_ALIGN`).
|
||||
// * `BLOCK_SIZE` is large and a multiple of 16.
|
||||
// * `chunk_ptr` and `alloc_ptr` have permission for writes.
|
||||
let arena = unsafe { Self::from_raw_parts(chunk_ptr, BLOCK_SIZE, alloc_ptr, ALLOC_LAYOUT) };
|
||||
|
||||
Some(arena)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user