Skip to content

Commit c1d243b

Browse files
committed
docs(allocator): improve docs for Allocator (#8623)
Improve docs for `Allocator`: 1. Explain how allocator works. 2. Demonstrate how to achieve good performance by re-using `Allocator`s. Also fix the doc test for `CloneIn`.
1 parent 8a0eb2a commit c1d243b

File tree

2 files changed

+213
-56
lines changed

2 files changed

+213
-56
lines changed

crates/oxc_allocator/src/clone_in.rs

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use crate::{Allocator, Box, Vec};
88
/// It'd only differ in the lifetime, Here's an example:
99
///
1010
/// ```
11+
/// # use oxc_allocator::{Allocator, CloneIn, Vec};
12+
/// # struct Struct<'a> {a: Vec<'a, u8>, b: u8}
13+
///
1114
/// impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for Struct<'old_alloc> {
1215
/// type Cloned = Struct<'new_alloc>;
1316
/// fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned {

crates/oxc_allocator/src/lib.rs

+210-56
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,16 @@
11
//! # ⚓ Oxc Memory Allocator
22
//!
3-
//! Oxc uses a bump-based memory arena for faster AST allocations. This crate
4-
//! contains an [`Allocator`] for creating such arenas, as well as ports of
5-
//! memory management data types from `std` adapted to use this arena.
3+
//! Oxc uses a bump-based memory arena for faster AST allocations.
64
//!
7-
//! ## No `Drop`s
5+
//! This crate contains an [`Allocator`] for creating such arenas, as well as ports of data types
6+
//! from `std` adapted to use this arena:
87
//!
9-
//! Objects allocated into Oxc memory arenas are never [`Dropped`](Drop).
10-
//! Memory is released in bulk when the allocator is dropped, without dropping the individual
11-
//! objects in the arena.
8+
//! * [`Box`]
9+
//! * [`Vec`]
10+
//! * [`String`]
11+
//! * [`HashMap`]
1212
//!
13-
//! Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
14-
//! which own memory allocations outside the arena.
15-
//!
16-
//! Static checks make this impossible to do. [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`],
17-
//! and all other methods which store data in the arena will refuse to compile if called with
18-
//! a [`Drop`] type.
19-
//!
20-
//! ## Examples
21-
//!
22-
//! ```ignore
23-
//! use oxc_allocator::{Allocator, Box};
24-
//!
25-
//! struct Foo {
26-
//! pub a: i32
27-
//! }
28-
//!
29-
//! impl std::ops::Drop for Foo {
30-
//! fn drop(&mut self) {}
31-
//! }
32-
//!
33-
//! struct Bar {
34-
//! v: std::vec::Vec<u8>,
35-
//! }
36-
//!
37-
//! let allocator = Allocator::default();
38-
//!
39-
//! // This will fail to compile because `Foo` implements `Drop`
40-
//! let foo = Box::new_in(Foo { a: 0 }, &allocator);
41-
//! // This will fail to compile because `Bar` contains a `std::vec::Vec`, and it implements `Drop`
42-
//! let bar = Box::new_in(Bar { v: vec![1, 2, 3] }, &allocator);
43-
//! ```
44-
//!
45-
//! Consumers of the [`oxc` umbrella crate](https://crates.io/crates/oxc) pass
46-
//! [`Allocator`] references to other tools.
47-
//!
48-
//! ```ignore
49-
//! use oxc::{allocator::Allocator, parser::Parser, span::SourceType};
50-
//!
51-
//! let allocator = Allocator::default();
52-
//! let parsed = Parser::new(&allocator, "let x = 1;", SourceType::default());
53-
//! assert!(parsed.errors.is_empty());
54-
//! ```
13+
//! See [`Allocator`] docs for information on efficient use of [`Allocator`].
5514
5615
#![warn(missing_docs)]
5716

@@ -76,13 +35,203 @@ pub use hash_map::HashMap;
7635
pub use string::String;
7736
pub use vec::Vec;
7837

79-
/// A bump-allocated memory arena based on [bumpalo].
38+
/// A bump-allocated memory arena.
39+
///
40+
/// # Anatomy of an Allocator
41+
///
42+
/// [`Allocator`] is flexibly sized. It grows as required as you allocate data into it.
43+
///
44+
/// To do that, an [`Allocator`] consists of multiple memory chunks.
45+
///
46+
/// [`Allocator::new`] creates a new allocator without any chunks. When you first allocate an object
47+
/// into it, it will lazily create an initial chunk, the size of which is determined by the size of that
48+
/// first allocation.
49+
///
50+
/// As more data is allocated into the [`Allocator`], it will likely run out of capacity. At that point,
51+
/// a new memory chunk is added, and further allocations will use this new chunk (until it too runs out
52+
/// of capacity, and *another* chunk is added).
53+
///
54+
/// The data from the 1st chunk is not copied into the 2nd one. It stays where it is, which means
55+
/// `&` or `&mut` references to data in the first chunk remain valid. This is unlike e.g. `Vec` which
56+
/// copies all existing data when it grows.
57+
///
58+
/// Each chunk is at least double the size of the last one, so growth in capacity is exponential.
59+
///
60+
/// [`Allocator::reset`] keeps only the last chunk (the biggest one), and discards any other chunks,
61+
/// returning their memory to the global allocator. The last chunk has its cursor rewound back to
62+
/// the start, so it's empty, ready to be re-used for allocating more data.
63+
///
64+
/// # Recycling allocators
65+
///
66+
/// For good performance, it's ideal to create an [`Allocator`], and re-use it over and over, rather than
67+
/// repeatedly creating and dropping [`Allocator`]s.
68+
///
69+
/// ```
70+
/// // This is good!
71+
/// use oxc_allocator::Allocator;
72+
/// let mut allocator = Allocator::new();
73+
///
74+
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
75+
/// for i in 0..100 {
76+
/// do_stuff(i, &allocator);
77+
/// // Reset the allocator, freeing the memory used by `do_stuff`
78+
/// allocator.reset();
79+
/// }
80+
/// ```
81+
///
82+
/// ```
83+
/// // DON'T DO THIS!
84+
/// # use oxc_allocator::Allocator;
85+
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
86+
/// for i in 0..100 {
87+
/// let allocator = Allocator::new();
88+
/// do_stuff(i, &allocator);
89+
/// }
90+
/// ```
91+
///
92+
/// ```
93+
/// // DON'T DO THIS EITHER!
94+
/// # use oxc_allocator::Allocator;
95+
/// # let allocator = Allocator::new();
96+
/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
97+
/// for i in 0..100 {
98+
/// do_stuff(i, &allocator);
99+
/// // We haven't reset the allocator, so we haven't freed the memory used by `do_stuff`.
100+
/// // The allocator will grow and grow, consuming more and more memory.
101+
/// }
102+
/// ```
103+
///
104+
/// ## Why is re-using an [`Allocator`] good for performance?
105+
///
106+
/// 3 reasons:
107+
///
108+
/// #### 1. Avoid expensive system calls
109+
///
110+
/// Creating an [`Allocator`] is a fairly expensive operation as it involves a call into global allocator,
111+
/// which in turn will likely make a system call. Ditto when the [`Allocator`] is dropped.
112+
/// Re-using an existing [`Allocator`] avoids these costs.
113+
///
114+
/// #### 2. CPU cache
115+
///
116+
/// Re-using an existing allocator means you're re-using the same block of memory. If that memory was
117+
/// recently accessed, it's likely to be warm in the CPU cache, so memory accesses will be much faster
118+
/// than accessing "cold" sections of main memory.
119+
///
120+
/// This can have a very significant positive impact on performance.
121+
///
122+
/// #### 3. Capacity stabilization
80123
///
81-
/// ## No `Drop`s
124+
/// The most efficient [`Allocator`] is one with only 1 chunk which has sufficient capacity for
125+
/// everything you're going to allocate into it.
82126
///
83-
/// Objects that are bump-allocated will never have their [`Drop`] implementation
84-
/// called &mdash; unless you do it manually yourself. This makes it relatively
85-
/// easy to leak memory or other resources.
127+
/// Why?
128+
///
129+
/// 1. Every allocation will occur without the allocator needing to grow.
130+
///
131+
/// 2. This makes the "is there sufficient capacity to allocate this?" check in [`alloc`] completely
132+
/// predictable (the answer is always "yes"). The CPU's branch predictor swiftly learns this,
133+
/// speeding up operation.
134+
///
135+
/// 3. When the [`Allocator`] is reset, there are no excess chunks to discard, so no system calls.
136+
///
137+
/// Because [`reset`] keeps only the biggest chunk (see above), re-using the same [`Allocator`]
138+
/// for multiple similar workloads will result in the [`Allocator`] swiftly stabilizing at a capacity
139+
/// which is sufficient to service those workloads with a single chunk.
140+
///
141+
/// If workload is completely uniform, it reaches stable state on the 3rd round.
142+
///
143+
/// ```
144+
/// # use oxc_allocator::Allocator;
145+
/// let mut allocator = Allocator::new();
146+
///
147+
/// fn workload(allocator: &Allocator) {
148+
/// // Allocate 4 MB of data in small chunks
149+
/// for i in 0..1_000_000u32 {
150+
/// allocator.alloc(i);
151+
/// }
152+
/// }
153+
///
154+
/// // 1st round
155+
/// workload(&allocator);
156+
///
157+
/// // `allocator` has capacity for 4 MB data, but split into many chunks.
158+
/// // `reset` throws away all chunks except the last one which will be approx 2 MB.
159+
/// allocator.reset();
160+
///
161+
/// // 2nd round
162+
/// workload(&allocator);
163+
///
164+
/// // `workload` filled the 2 MB chunk, so a 2nd chunk was created of double the size (4 MB).
165+
/// // `reset` discards the smaller chunk, leaving only a single 4 MB chunk.
166+
/// allocator.reset();
167+
///
168+
/// // 3rd round
169+
/// // `allocator` now has sufficient capacity for all allocations in a single 4 MB chunk.
170+
/// workload(&allocator);
171+
///
172+
/// // `reset` has no chunks to discard. It keeps the single 4 MB chunk. No system calls.
173+
/// allocator.reset();
174+
///
175+
/// // More rounds
176+
/// // All serviced without needing to grow the allocator, and with no system calls.
177+
/// for _ in 0..100 {
178+
/// workload(&allocator);
179+
/// allocator.reset();
180+
/// }
181+
/// ```
182+
///
183+
/// [`reset`]: Allocator::reset
184+
/// [`alloc`]: Allocator::alloc
185+
///
186+
/// # No `Drop`s
187+
///
188+
/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop).
189+
/// Memory is released in bulk when the allocator is dropped, without dropping the individual
190+
/// objects in the arena.
191+
///
192+
/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
193+
/// which own memory allocations outside the arena.
194+
///
195+
/// Static checks make this impossible to do. [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`],
196+
/// [`HashMap::new_in`], and all other methods which store data in the arena will refuse to compile
197+
/// if called with a [`Drop`] type.
198+
///
199+
/// ```ignore
200+
/// use oxc_allocator::{Allocator, Box};
201+
///
202+
/// let allocator = Allocator::new();
203+
///
204+
/// struct Foo {
205+
/// pub a: i32
206+
/// }
207+
///
208+
/// impl std::ops::Drop for Foo {
209+
/// fn drop(&mut self) {}
210+
/// }
211+
///
212+
/// // This will fail to compile because `Foo` implements `Drop`
213+
/// let foo = Box::new_in(Foo { a: 0 }, &allocator);
214+
///
215+
/// struct Bar {
216+
/// v: std::vec::Vec<u8>,
217+
/// }
218+
///
219+
/// // This will fail to compile because `Bar` contains a `std::vec::Vec`, and it implements `Drop`
220+
/// let bar = Box::new_in(Bar { v: vec![1, 2, 3] }, &allocator);
221+
/// ```
222+
///
223+
/// # Examples
224+
///
225+
/// Consumers of the [`oxc` umbrella crate](https://crates.io/crates/oxc) pass
226+
/// [`Allocator`] references to other tools.
227+
///
228+
/// ```ignore
229+
/// use oxc::{allocator::Allocator, parser::Parser, span::SourceType};
230+
///
231+
/// let allocator = Allocator::default();
232+
/// let parsed = Parser::new(&allocator, "let x = 1;", SourceType::default());
233+
/// assert!(parsed.errors.is_empty());
234+
/// ```
86235
#[derive(Default)]
87236
pub struct Allocator {
88237
bump: Bump,
@@ -96,9 +245,12 @@ impl Allocator {
96245
/// (e.g. with [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`], [`HashMap::new_in`]).
97246
///
98247
/// If you can estimate the amount of memory the allocator will require to fit what you intend to
99-
/// allocate into it, it is generally preferable to create that allocator with [`with_capacity`]
248+
/// allocate into it, it is generally preferable to create that allocator with [`with_capacity`],
100249
/// which reserves that amount of memory upfront. This will avoid further system calls to allocate
101-
/// further chunks later on.
250+
/// further chunks later on. This point is less important if you're re-using the allocator multiple
251+
/// times.
252+
///
253+
/// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
102254
///
103255
/// [`with_capacity`]: Allocator::with_capacity
104256
//
@@ -110,6 +262,8 @@ impl Allocator {
110262
}
111263

112264
/// Create a new [`Allocator`] with specified capacity.
265+
///
266+
/// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
113267
//
114268
// `#[inline(always)]` because just delegates to `bumpalo` method
115269
#[expect(clippy::inline_always)]

0 commit comments

Comments
 (0)