1
1
//! # ⚓ Oxc Memory Allocator
2
2
//!
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.
6
4
//!
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:
8
7
//!
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`]
12
12
//!
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`].
55
14
56
15
#![ warn( missing_docs) ]
57
16
@@ -76,13 +35,203 @@ pub use hash_map::HashMap;
76
35
pub use string:: String ;
77
36
pub use vec:: Vec ;
78
37
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
80
123
///
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.
82
126
///
83
- /// Objects that are bump-allocated will never have their [`Drop`] implementation
84
- /// called — 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
+ /// ```
86
235
#[ derive( Default ) ]
87
236
pub struct Allocator {
88
237
bump : Bump ,
@@ -96,9 +245,12 @@ impl Allocator {
96
245
/// (e.g. with [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`], [`HashMap::new_in`]).
97
246
///
98
247
/// 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`],
100
249
/// 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`].
102
254
///
103
255
/// [`with_capacity`]: Allocator::with_capacity
104
256
//
@@ -110,6 +262,8 @@ impl Allocator {
110
262
}
111
263
112
264
/// Create a new [`Allocator`] with specified capacity.
265
+ ///
266
+ /// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
113
267
//
114
268
// `#[inline(always)]` because just delegates to `bumpalo` method
115
269
#[ expect( clippy:: inline_always) ]
0 commit comments