commonware_allocator/mimalloc/mod.rs
1//! [mimalloc](https://github.com/microsoft/mimalloc) is a general-purpose
2//! allocator that serves each size class from thread-local pages carved out
3//! of arenas of operating system memory, returns an object freed by any
4//! thread to the page it came from, and purges freed arena memory back to
5//! the operating system after a delay.
6//!
7//! This module installs mimalloc as the global allocator through
8//! [`MiMalloc`], a [`GlobalAlloc`] whose [`Config`] lives in the binary and
9//! is applied by the first allocation. [`Effective`] reports the options in
10//! force and [`Stats`] reports allocator statistics.
11//!
12//! # Configuration timing
13//!
14//! mimalloc creates its first arena at the first allocation, which happens
15//! before `main` on most targets. [`MiMalloc`] applies its [`Config`] on that
16//! allocation, so the purge policy is in force before any memory is freed and
17//! a reservation is the first arena threads search. A reservation requested
18//! through the environment (`MIMALLOC_RESERVE_OS_MEMORY` or
19//! `MIMALLOC_RESERVE_HUGE_OS_PAGES`) is made at load and comes first.
20//!
21//! # Linking
22//!
23//! The crate declares `links = "mimalloc"`, so Cargo rejects a dependency
24//! graph in which another package, such as `libmimalloc-sys`, declares the
25//! same key.
26//!
27//! Enable the `local-dynamic-tls` feature when the crate is built into a
28//! shared library that is loaded with `dlopen`. Without it, mimalloc uses
29//! initial-exec thread-local storage on targets other than musl. That storage
30//! comes from a static block the loader sizes at startup, and a library
31//! loaded later may not fit in it. The feature also has mimalloc free what
32//! memory it can when the library is unloaded.
33//!
34//! # Page geometry
35//!
36//! mimalloc carves arenas into slices and derives its page sizes from the
37//! slice. Slices form chunks, and chunks form the metadata groups of an
38//! arena. One group looks like this with the default slice on 64-bit targets:
39//!
40//! ```text
41//! one metadata group: 8 chunks, 256 MiB
42//! |<----- ARENA_MAX_OBJECT_SIZE: 7 chunks, 224 MiB ------>|
43//! +-------+-------+-------+-------+-------+---------------+-------+
44//! |chunk 0|chunk 1|chunk 2|chunk 3|chunk 4|page of its own|chunk 7|
45//! +-------+-------+-------+-------+-------+---------------+-------+
46//! | |
47//! | '-------------------------------------------------------.
48//! | |
49//! +--------+--------+---------+-----------------+-----------------+
50//! | page | small | medium | large | |
51//! |metadata|1 slice |8 slices | 64 slices | ... |
52//! | table | 64 KiB | 512 KiB | 4 MiB | |
53//! +--------+--------+---------+-----------------+-----------------+
54//! |<------>| ARENA_SLICE_SIZE
55//! |<----------- ARENA_CHUNK_SIZE: 512 slices, 32 MiB ------------>|
56//! ```
57//!
58//! Objects up to [`LARGE_OBJECT_LIMIT`] come from thread-local pages at the
59//! cost of a free-list operation. Larger objects get a page of their own,
60//! which costs an arena operation per allocation and free.
61//!
62//! The crate's `large-objects-*` features raise the limit at build time by
63//! scaling the slice ([`ARENA_SLICE_SIZE`]) and with it every page. The
64//! geometry applies to the whole process, so only binaries should enable
65//! them. The largest enabled feature wins.
66//!
67//! # Large objects
68//!
69//! Where an allocation lives depends on its size and alignment:
70//!
71//! ```text
72//! allocation (size, alignment)
73//! |
74//! +-- alignment >= metadata group (256 MiB) ---> null
75//! |
76//! +-- alignment > ARENA_SLICE_SIZE (64 KiB) ---> page of its own,
77//! | mapped from the OS
78//! v
79//! size <= LARGE_OBJECT_LIMIT (512 KiB)?
80//! | yes | no
81//! v v
82//! shared page of its size class page of its own
83//! | |
84//! '----------------+----------------'
85//! v
86//! page <= Config::arena_max_object_size?
87//! | yes | no
88//! v v
89//! arena -- no room, no new arena ---> OS mapping
90//! ```
91//!
92//! A shared page outlives its objects for reuse. A page of its own is sized
93//! to the object rounded up (by as much as 4 MiB for the largest), and
94//! freeing the object releases it. The exception is a small object aligned
95//! beyond a slice and freed by another thread, which holds its page until the
96//! owning thread collects.
97//!
98//! An arena keeps a released page until the [`Purge`] policy purges it. A
99//! page spanning several chunks (see [`ARENA_CHUNK_SIZE`]) needs that many
100//! consecutive free chunks, so a fragmented arena can make mimalloc reserve a
101//! new arena or, failing that, map the page from the operating system. Such
102//! a page is unmapped on release, so each allocation pays for system calls
103//! and fresh page faults.
104//!
105//! Pages above [`Config::arena_max_object_size`] are mapped from the
106//! operating system the same way. The limit defaults to and is capped at
107//! [`ARENA_MAX_OBJECT_SIZE`], the largest page an arena can hold. A limit below
108//! the largest shared page also maps the shared pages of larger size classes
109//! from the operating system. Those pages are still kept for reuse.
110//!
111//! Alignments of a whole metadata group or more are refused with null, which
112//! a [`GlobalAlloc`] caller reports as an allocation error.
113//!
114//! # Example
115//!
116//! ```no_run
117//! use commonware_allocator::mimalloc::{Config, MiMalloc, Purge, Reserve, Status};
118//! use std::time::Duration;
119//!
120//! #[global_allocator]
121//! static GLOBAL: MiMalloc = MiMalloc::new(Config {
122//! purge: Purge::After(Duration::from_secs(10)),
123//! reserve: Some(Reserve::pinned(2 << 30)),
124//! ..Config::DEFAULT
125//! });
126//!
127//! fn main() {
128//! assert!(commonware_allocator::mimalloc::is_global_allocator());
129//! println!("{:?}", Status::read());
130//! }
131//! ```
132
133mod ffi;
134pub mod metrics;
135
136use ffi as mi;
137use std::{
138 alloc::{GlobalAlloc, Layout},
139 ffi::{c_long, c_void},
140 hint::spin_loop,
141 mem, ptr,
142 sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
143 time::Duration,
144};
145
146/// mimalloc options the wrapper reads or writes, numbered as `mi_option_t` in
147/// the vendored `mimalloc.h`. A test checks the values against that header.
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149#[repr(i32)]
150enum OptionId {
151 /// Commit policy for arenas mimalloc reserves on demand: 0 on first use,
152 /// 1 eagerly, 2 eagerly where the system overcommits or large OS pages
153 /// are allowed.
154 ArenaEagerCommit = 4,
155 /// Whether a purge decommits memory rather than resetting it (flag).
156 PurgeDecommits = 5,
157 /// Whether arenas may be backed by large OS pages (flag).
158 AllowLargeOsPages = 6,
159 /// Milliseconds a freed arena range waits before it may be purged: -1
160 /// never purges, 0 purges on free.
161 PurgeDelay = 15,
162 /// Base size of arenas reserved on demand, in KiB.
163 ArenaReserve = 23,
164 /// Factor applied to the purge delay for arena memory.
165 ArenaPurgeMult = 24,
166 /// Transparent huge pages: 0 disallowed, 1 allowed, 2 allowed with
167 /// purging in whole huge pages.
168 AllowThp = 43,
169 /// Largest page an arena may hold, in KiB.
170 ArenaMaxObjectSize = 45,
171}
172
173impl OptionId {
174 fn get(self) -> c_long {
175 mi::mi_option_get(self as mi::mi_option_t)
176 }
177
178 fn set(self, value: c_long) {
179 // SAFETY: callers pass flags, KiB sizes bounded by `size_option`, or
180 // delays bounded by `MAX_PURGE_DELAY`. They run inside `apply` under
181 // the `OptionsGuard`, so no reader observes a partial write.
182 unsafe { mi::mi_option_set(self as mi::mi_option_t, value) }
183 }
184}
185
186/// Bytes handed to mimalloc as pinned arenas. mimalloc accounts only for
187/// memory it mapped itself, so these are reported separately in
188/// [`Stats::pinned_bytes`].
189static PINNED_BYTES: AtomicU64 = AtomicU64::new(0);
190
191/// Serializes access to mimalloc's option table, which holds plain C values.
192/// Configuration and [`Effective::read`] hold it. mimalloc reads options only
193/// inside allocations, which wait until configuration is published. The guard
194/// never allocates because configuration runs inside `alloc`.
195static OPTIONS_LOCK: AtomicBool = AtomicBool::new(false);
196
197struct OptionsGuard;
198
199impl OptionsGuard {
200 fn lock() -> Self {
201 while OPTIONS_LOCK
202 .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
203 .is_err()
204 {
205 spin_loop();
206 }
207 Self
208 }
209}
210
211impl Drop for OptionsGuard {
212 fn drop(&mut self) {
213 OPTIONS_LOCK.store(false, Ordering::Release);
214 }
215}
216
217/// Log2 of the arena slice size. Without a `large-objects-*` feature it is
218/// mimalloc's default (`13 + MI_SIZE_SHIFT` in the vendored `types.h`).
219const ARENA_SLICE_SHIFT: u32 = if cfg!(feature = "large-objects-4mb") {
220 19
221} else if cfg!(feature = "large-objects-2mb") {
222 18
223} else if cfg!(feature = "large-objects-1mb") {
224 17
225} else {
226 13 + size_of::<usize>().trailing_zeros()
227};
228
229/// Size of an arena slice, the unit mimalloc carves arenas into and the
230/// smallest page size.
231pub const ARENA_SLICE_SIZE: usize = 1 << ARENA_SLICE_SHIFT;
232
233/// Size of an arena chunk, the unit reservations are rounded down to (see
234/// [`Reserve::bytes`]). It is 512 slices, or 32 MiB at the default slice on
235/// 64-bit targets.
236pub const ARENA_CHUNK_SIZE: usize = 64 * size_of::<usize>() * ARENA_SLICE_SIZE;
237
238/// Largest allocation served from a shared thread-local page. It is eight
239/// slices: 512 KiB by default, or the size named by an enabled
240/// `large-objects-*` feature. Larger objects get a page of their own, which
241/// costs an arena operation per allocation and free.
242pub const LARGE_OBJECT_LIMIT: usize = size_of::<usize>() * ARENA_SLICE_SIZE;
243
244/// Largest page an arena can hold, and so the cap on
245/// [`Config::arena_max_object_size`]. mimalloc keeps page metadata at the
246/// start of every metadata group, so a page spans at most the group's other
247/// chunks: seven chunks, or 224 MiB at the default slice on 64-bit targets.
248pub const ARENA_MAX_OBJECT_SIZE: usize = (size_of::<usize>() - 1) * ARENA_CHUNK_SIZE;
249
250/// When memory freed back to an arena is purged (decommitted) to the
251/// operating system.
252///
253/// The policy covers arena memory only. Pages above
254/// [`Config::arena_max_object_size`] and objects aligned beyond a slice are
255/// mapped directly and unmapped on release. Pinned reservations (see
256/// [`Reserve`]) are never purged.
257///
258/// When the kernel supports transparent huge pages and they are allowed (the
259/// default), a purge releases only whole 2 MiB granules freed since the
260/// arena's last purge. A smaller freed range stays resident until its
261/// neighbors are freed too. The environment can select a finer granule with
262/// `MIMALLOC_ALLOW_THP=1` or `MIMALLOC_MINIMAL_PURGE_SIZE`.
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
264pub enum Purge {
265 /// mimalloc's default: its purge delay times its arena multiplier
266 /// (four seconds in mimalloc 3.5).
267 Default,
268 /// Never purge arena memory.
269 Never,
270 /// Purge freed memory after a delay. The first free into an arena after a
271 /// purge sets a deadline this far ahead, and the arena is purged at the
272 /// first collection after it. A thread collects every 10,000 allocations
273 /// that miss its fast path by default, so a process that allocates rarely
274 /// can keep freed memory well past the deadline. Delays above
275 /// [`MAX_PURGE_DELAY`] are clamped, and one below a millisecond purges
276 /// each page as it is freed.
277 After(Duration),
278}
279
280/// Longest purge delay [`Purge::After`] applies (about 24 days). mimalloc
281/// stores the delay in milliseconds in a C `long`, which is 32 bits on some
282/// platforms.
283pub const MAX_PURGE_DELAY: Duration = Duration::from_millis(i32::MAX as u64);
284
285/// Memory reserved from the operating system before the first allocation.
286#[derive(Clone, Copy, Debug, PartialEq, Eq)]
287pub struct Reserve {
288 /// Bytes to reserve, rounded down to whole [`ARENA_CHUNK_SIZE`] chunks. A
289 /// reservation below one chunk fails.
290 pub bytes: usize,
291 /// Pinned memory is mapped by this crate and handed to mimalloc as an
292 /// arena that is never purged. Unpinned memory is reserved through
293 /// mimalloc and purged like any other arena, unless the kernel granted it
294 /// large OS pages. On Linux, both get transparent huge page advice when
295 /// [`Config::transparent_huge_pages`] allows it.
296 pub pinned: bool,
297 /// Touch every page of a pinned reservation up front so first use never
298 /// faults. Ignored for unpinned reservations.
299 pub prefault: bool,
300}
301
302impl Reserve {
303 /// A pinned reservation that is faulted in on first use.
304 pub const fn pinned(bytes: usize) -> Self {
305 Self {
306 bytes,
307 pinned: true,
308 prefault: false,
309 }
310 }
311
312 /// A reservation managed and purged by mimalloc.
313 pub const fn unpinned(bytes: usize) -> Self {
314 Self {
315 bytes,
316 pinned: false,
317 prefault: false,
318 }
319 }
320}
321
322/// Configuration applied by [`MiMalloc`] on its first allocation.
323///
324/// Every field can keep mimalloc's current setting, including any value set
325/// through the environment.
326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
327pub struct Config {
328 /// When freed memory is purged back to the operating system.
329 pub purge: Purge,
330 /// Memory reserved before the first allocation.
331 pub reserve: Option<Reserve>,
332 /// Whether mimalloc may back arenas with large (2 MiB) OS pages when the
333 /// kernel grants them. `None` keeps the current setting. Large-page
334 /// arenas are never purged, are not swappable, and come from the kernel's
335 /// huge page pool.
336 pub allow_large_os_pages: Option<bool>,
337 /// Whether the process may use transparent huge pages. `None` keeps the
338 /// current setting, which allows them unless the environment disabled
339 /// them. On Linux this also sets the process flag the kernel consults for
340 /// every mapping. If `MIMALLOC_ALLOW_THP=0` was set at load, `Some(true)`
341 /// restores that flag, but mimalloc still does not advise huge pages for
342 /// its own mappings.
343 pub transparent_huge_pages: Option<bool>,
344 /// Whether the arenas mimalloc reserves on its own are committed eagerly
345 /// rather than on first use. `None` keeps mimalloc's default (eager on
346 /// systems that overcommit or when large OS pages are allowed).
347 pub arena_eager_commit: Option<bool>,
348 /// Base size of the arenas mimalloc reserves on demand once existing
349 /// arenas are full, rounded up to KiB. mimalloc reserves at least two
350 /// chunks per arena and grows the size with the arena count and for
351 /// larger requests. `None` keeps mimalloc's default (1 GiB).
352 ///
353 /// `Some(0)` disables on-demand arenas. Pages that no existing arena can
354 /// hold are then mapped from the operating system and unmapped on
355 /// release.
356 pub arena_reserve: Option<usize>,
357 /// Largest page carved from an arena, in bytes, rounded up to a slice and
358 /// clamped between one slice and [`ARENA_MAX_OBJECT_SIZE`]. Larger pages
359 /// are mapped from the operating system and unmapped on release, so a
360 /// lower limit keeps occasional huge buffers from holding arena memory.
361 /// `None` keeps mimalloc's setting under the same cap. See the module
362 /// docs on large objects.
363 pub arena_max_object_size: Option<usize>,
364}
365
366impl Config {
367 /// Leaves every mimalloc option at its default, capping only the arena
368 /// object-size limit at [`ARENA_MAX_OBJECT_SIZE`].
369 pub const DEFAULT: Self = Self {
370 purge: Purge::Default,
371 reserve: None,
372 allow_large_os_pages: None,
373 transparent_huge_pages: None,
374 arena_eager_commit: None,
375 arena_reserve: None,
376 arena_max_object_size: None,
377 };
378}
379
380impl Default for Config {
381 fn default() -> Self {
382 Self::DEFAULT
383 }
384}
385
386/// Outcome of the reservation requested by [`Config::reserve`].
387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
388pub enum ReserveOutcome {
389 /// No reservation was requested.
390 NotRequested,
391 /// The reservation is a pinned arena that is never purged.
392 Pinned,
393 /// The reservation is an ordinary arena managed by mimalloc.
394 Reserved,
395 /// The operating system or mimalloc refused the reservation. Allocation
396 /// continues from on-demand arenas.
397 Failed,
398}
399
400/// Process-wide configuration state.
401#[derive(Clone, Copy, Debug, PartialEq, Eq)]
402pub struct Status {
403 /// Whether a [`MiMalloc`] has configured the process, which happens on
404 /// its first allocation.
405 pub configured: bool,
406 /// Outcome of the requested reservation.
407 pub reserve: ReserveOutcome,
408}
409
410impl Status {
411 /// Reads whether the process has been configured and how the
412 /// reservation went.
413 pub fn read() -> Self {
414 let Some(outcome) = STATE.load(Ordering::Acquire).checked_sub(1) else {
415 return Self {
416 configured: false,
417 reserve: ReserveOutcome::NotRequested,
418 };
419 };
420 let reserve = match outcome {
421 x if x == ReserveOutcome::Pinned as u8 => ReserveOutcome::Pinned,
422 x if x == ReserveOutcome::Reserved as u8 => ReserveOutcome::Reserved,
423 x if x == ReserveOutcome::Failed as u8 => ReserveOutcome::Failed,
424 _ => ReserveOutcome::NotRequested,
425 };
426 Self {
427 configured: true,
428 reserve,
429 }
430 }
431}
432
433/// Largest alignment `mi_malloc` can satisfy (`MI_MAX_ALIGN_SIZE` in the
434/// vendored `types.h`, checked by a test). A size that is a multiple of 16
435/// maps to a size class whose blocks are all 16-byte aligned.
436const MALLOC_ALIGNMENT: usize = 16;
437
438/// Alignment every `mi_malloc` block has regardless of size.
439const WORD_ALIGNMENT: usize = size_of::<usize>();
440
441/// Whether a plain `mi_malloc` block of `size` bytes is aligned to `align`,
442/// so the aligned entry points can be skipped.
443#[inline]
444const fn malloc_satisfies(size: usize, align: usize) -> bool {
445 align <= WORD_ALIGNMENT || (align <= MALLOC_ALIGNMENT && size & (align - 1) == 0)
446}
447
448/// Zero until a [`MiMalloc`] has configured the process, then the
449/// [`ReserveOutcome`] plus one.
450static STATE: AtomicU8 = AtomicU8::new(0);
451
452/// mimalloc as a `#[global_allocator]` with a [`Config`] applied on the first
453/// allocation.
454///
455/// Configuration is process-wide. The first instance to serve an allocation
456/// applies its [`Config`], and later instances never apply theirs.
457/// [`Status::read`] reports the outcome.
458pub struct MiMalloc {
459 config: Config,
460}
461
462impl MiMalloc {
463 /// Creates an allocator that requests `config`.
464 pub const fn new(config: Config) -> Self {
465 Self { config }
466 }
467
468 /// Returns the configuration this allocator requests.
469 pub const fn config(&self) -> &Config {
470 &self.config
471 }
472
473 #[inline]
474 fn ensure_configured(&self) {
475 if STATE.load(Ordering::Acquire) == 0 {
476 self.configure();
477 }
478 }
479
480 /// Applies the configuration once. Concurrent first allocations wait on
481 /// the options guard and then find the state set. Nothing here allocates
482 /// through Rust, so the first allocation cannot recurse.
483 #[cold]
484 #[inline(never)]
485 fn configure(&self) {
486 let _guard = OptionsGuard::lock();
487 if STATE.load(Ordering::Relaxed) != 0 {
488 return;
489 }
490 let outcome = apply(&self.config);
491 STATE.store(outcome as u8 + 1, Ordering::Release);
492 }
493}
494
495// SAFETY: mimalloc is thread-safe and the configuration state is accessed
496// through atomics. Layouts that `malloc_satisfies` rejects use the aligned
497// entry points, and mimalloc frees and reallocates blocks from either.
498unsafe impl GlobalAlloc for MiMalloc {
499 #[inline]
500 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
501 self.ensure_configured();
502 if malloc_satisfies(layout.size(), layout.align()) {
503 mi::mi_malloc(layout.size()) as *mut u8
504 } else {
505 mi::mi_malloc_aligned(layout.size(), layout.align()) as *mut u8
506 }
507 }
508
509 #[inline]
510 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
511 self.ensure_configured();
512 if malloc_satisfies(layout.size(), layout.align()) {
513 mi::mi_zalloc(layout.size()) as *mut u8
514 } else {
515 mi::mi_zalloc_aligned(layout.size(), layout.align()) as *mut u8
516 }
517 }
518
519 #[inline]
520 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
521 // The layout lets mimalloc find a small block's page without looking
522 // up its metadata.
523 // SAFETY: GlobalAlloc requires `ptr` to be a block currently
524 // allocated by this allocator with `layout`, so it has at least
525 // `layout.size()` usable bytes and is aligned to `layout.align()`.
526 unsafe { mi::mi_free_size_aligned(ptr as *mut c_void, layout.size(), layout.align()) }
527 }
528
529 #[inline]
530 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
531 // Route by the new size. When plain `mi_malloc` satisfies it, the
532 // block `mi_realloc` returns is aligned whether it stays in place or
533 // moves, and a moved block lands in its exact size class. The aligned
534 // entry point can pad a moved block into the next class.
535 // SAFETY: GlobalAlloc requires ptr to come from this allocator with the given layout.
536 unsafe {
537 if malloc_satisfies(new_size, layout.align()) {
538 mi::mi_realloc(ptr as *mut c_void, new_size) as *mut u8
539 } else {
540 mi::mi_realloc_aligned(ptr as *mut c_void, new_size, layout.align()) as *mut u8
541 }
542 }
543 }
544}
545
546/// Applies `config` to the process-wide mimalloc instance. The caller holds
547/// the [`OptionsGuard`].
548fn apply(config: &Config) -> ReserveOutcome {
549 mi::mi_process_init();
550 match config.purge {
551 Purge::Default => {}
552 Purge::Never => OptionId::PurgeDelay.set(-1),
553 Purge::After(delay) => {
554 // mimalloc 3 purges only arenas and scales their delay by the
555 // arena multiplier, so a multiplier of one applies the delay as
556 // given.
557 OptionId::PurgeDelay.set(purge_millis(delay));
558 OptionId::ArenaPurgeMult.set(1);
559 }
560 }
561 if let Some(eager) = config.arena_eager_commit {
562 OptionId::ArenaEagerCommit.set(c_long::from(eager));
563 }
564 if let Some(allowed) = config.allow_large_os_pages {
565 OptionId::AllowLargeOsPages.set(c_long::from(allowed));
566 }
567 if let Some(allowed) = config.transparent_huge_pages {
568 set_transparent_huge_pages(allowed);
569 }
570 if let Some(bytes) = config.arena_reserve {
571 OptionId::ArenaReserve.set(size_option(bytes.div_ceil(1024)));
572 }
573
574 // Cap the limit from any source at `ARENA_MAX_OBJECT_SIZE`. mimalloc's own
575 // ceiling admits pages no arena can hold, and it reserves a futile arena
576 // for each before mapping from the operating system.
577 let limit = config.arena_max_object_size.map_or_else(
578 || OptionId::ArenaMaxObjectSize.get(),
579 |bytes| size_option(bytes.div_ceil(1024)),
580 );
581 OptionId::ArenaMaxObjectSize.set(limit.min(size_option(ARENA_MAX_OBJECT_SIZE / 1024)));
582 match config.reserve {
583 None => ReserveOutcome::NotRequested,
584 Some(reserve) => {
585 let Some(size) = whole_chunks(reserve.bytes) else {
586 return ReserveOutcome::Failed;
587 };
588 if reserve.pinned {
589 return manage_pinned(size, reserve.prefault);
590 }
591
592 // Allow large pages as mimalloc does for its on-demand arenas. Its
593 // mapping code checks `allow_large_os_pages` first, and the
594 // permission also enables transparent huge page advice.
595 // SAFETY: the arena identifier output is null, so nothing is
596 // written through it.
597 let err =
598 unsafe { mi::mi_reserve_os_memory_ex(size, true, true, false, ptr::null_mut()) };
599 if err == 0 {
600 ReserveOutcome::Reserved
601 } else {
602 ReserveOutcome::Failed
603 }
604 }
605 }
606}
607
608/// Rounds `bytes` down to whole arena chunks, or `None` below one chunk.
609/// mimalloc uses only whole chunks, so the remainder is never mapped.
610fn whole_chunks(bytes: usize) -> Option<usize> {
611 let chunk = mi::mi_arena_min_size();
612 Some(bytes / chunk * chunk).filter(|size| *size > 0)
613}
614
615/// Bounds a size option in KiB so mimalloc's value in bytes fits a `c_long`.
616fn size_option(kib: usize) -> c_long {
617 kib.min(c_long::MAX as usize / 1024) as c_long
618}
619
620/// A purge delay in the milliseconds mimalloc stores, bounded by
621/// [`MAX_PURGE_DELAY`].
622fn purge_millis(delay: Duration) -> c_long {
623 delay.min(MAX_PURGE_DELAY).as_millis() as c_long
624}
625
626/// Computes the arena purge delay from the raw options as mimalloc does. A
627/// negative factor disables purging, and an overflowing product falls back to
628/// the bare delay.
629fn arena_purge_delay(delay: c_long, mult: c_long) -> Option<Duration> {
630 if delay < 0 || mult < 0 {
631 return None;
632 }
633 let (delay, mult) = (delay as u64, mult as u64);
634 let total = delay
635 .checked_mul(mult)
636 .filter(|total| c_long::try_from(*total).is_ok())
637 .unwrap_or(delay);
638 Some(Duration::from_millis(total))
639}
640
641/// Applies the transparent huge page policy.
642///
643/// mimalloc sets the Linux process flag only when it initializes at load,
644/// while the option keeps steering its huge page advice and purge
645/// granularity. The flag is therefore set here in both directions.
646fn set_transparent_huge_pages(allowed: bool) {
647 // mimalloc's default (2) allows huge pages and purges at huge-page
648 // granularity so purging does not split them.
649 OptionId::AllowThp.set(if allowed { 2 } else { 0 });
650 #[cfg(target_os = "linux")]
651 // SAFETY: this prctl takes integer arguments only.
652 unsafe {
653 libc::prctl(
654 libc::PR_SET_THP_DISABLE,
655 libc::c_ulong::from(!allowed),
656 0,
657 0,
658 0,
659 );
660 }
661}
662
663/// Maps `size` bytes of anonymous memory, a whole number of chunks, and
664/// hands them to mimalloc as a pinned, committed arena.
665///
666/// The mapping includes slack for mimalloc's arena alignment and is trimmed
667/// at both ends before the handover.
668fn manage_pinned(size: usize, prefault: bool) -> ReserveOutcome {
669 let alignment = mi::mi_arena_min_alignment();
670 let Some(len) = size.checked_add(alignment) else {
671 return ReserveOutcome::Failed;
672 };
673 // SAFETY: anonymous mapping with no file descriptor.
674 let base = unsafe {
675 libc::mmap(
676 ptr::null_mut(),
677 len,
678 libc::PROT_READ | libc::PROT_WRITE,
679 libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
680 -1,
681 0,
682 )
683 };
684 if base == libc::MAP_FAILED {
685 return ReserveOutcome::Failed;
686 }
687 let Some(start) = (base as usize).checked_next_multiple_of(alignment) else {
688 // SAFETY: the mapping was created above and never handed out.
689 unsafe { libc::munmap(base, len) };
690 return ReserveOutcome::Failed;
691 };
692 let head = start - base as usize;
693 let tail = len - head - size;
694 // SAFETY: both slack ranges lie inside the mapping created above and are
695 // never handed to mimalloc.
696 unsafe {
697 if head > 0 {
698 libc::munmap(base, head);
699 }
700 if tail > 0 {
701 libc::munmap((start + size) as *mut c_void, tail);
702 }
703 }
704
705 // Advise huge pages as mimalloc does for its own arenas, so a kernel in
706 // madvise mode backs the reservation with them.
707 #[cfg(target_os = "linux")]
708 if OptionId::AllowThp.get() != 0 {
709 // SAFETY: the range is a live private mapping owned by this function.
710 unsafe { libc::madvise(start as *mut c_void, size, libc::MADV_HUGEPAGE) };
711 }
712 if prefault {
713 // SAFETY: every touched address lies inside the retained range.
714 unsafe {
715 let page = libc::sysconf(libc::_SC_PAGESIZE).max(4096) as usize;
716 let mut offset = 0;
717 while offset < size {
718 ptr::write_volatile((start + offset) as *mut u8, 0);
719 offset += page;
720 }
721 }
722 }
723
724 // SAFETY: the range is a live private mapping that nothing else uses.
725 // mimalloc owns it for the rest of the process if the call returns true.
726 let managed = unsafe {
727 mi::mi_manage_os_memory_ex(
728 start as *mut c_void,
729 size,
730 true,
731 true,
732 true,
733 -1,
734 false,
735 ptr::null_mut(),
736 )
737 };
738 if managed {
739 PINNED_BYTES.fetch_add(size as u64, Ordering::Relaxed);
740 ReserveOutcome::Pinned
741 } else {
742 // SAFETY: mimalloc keeps no reference to a mapping it refused, so the
743 // aligned remainder is still owned here and unused.
744 unsafe { libc::munmap(start as *mut c_void, size) };
745 ReserveOutcome::Failed
746 }
747}
748
749/// Returns whether the global allocator is this crate's mimalloc, installed
750/// directly through [`MiMalloc`] or wrapped by another allocator. It checks a
751/// probe allocation against mimalloc's heap region.
752pub fn is_global_allocator() -> bool {
753 let probe = Box::new(0u8);
754 mi::mi_is_in_heap_region(ptr::from_ref::<u8>(&probe).cast())
755}
756
757/// Returns the linked mimalloc version as `major * 10000 + minor * 100 + patch`.
758pub fn version() -> u32 {
759 mi::mi_version() as u32
760}
761
762/// Returns the block size mimalloc allocates for `size` bytes: its size
763/// class, or above the largest class, `size` rounded up to an OS page. The
764/// page serving such a block may be larger.
765pub fn good_size(size: usize) -> usize {
766 mi::mi_good_size(size)
767}
768
769/// The options mimalloc is running with, whether set by a [`Config`], the
770/// environment, or its defaults.
771#[derive(Clone, Copy, Debug, PartialEq, Eq)]
772pub struct Effective {
773 /// Arena purge delay (mimalloc's purge delay times its arena multiplier),
774 /// or `None` if purging is disabled.
775 pub purge: Option<Duration>,
776 /// Whether a purge decommits memory rather than resetting it.
777 pub purge_decommits: bool,
778 /// Arena commit policy as mimalloc stores it (0 lazy, 1 eager, 2 eager on
779 /// systems that overcommit or when large OS pages are allowed).
780 pub arena_eager_commit: i64,
781 /// Whether arenas may use large OS pages.
782 pub allow_large_os_pages: bool,
783 /// Whether mimalloc may use transparent huge pages.
784 pub transparent_huge_pages: bool,
785 /// Base size of arenas reserved on demand, before mimalloc grows it with
786 /// the arena count.
787 pub arena_reserve: usize,
788 /// Largest page carved from an arena: the configured limit rounded up to
789 /// a slice and clamped between one slice and [`ARENA_MAX_OBJECT_SIZE`].
790 pub arena_max_object_size: usize,
791}
792
793impl Effective {
794 /// Reads the current option values.
795 pub fn read() -> Self {
796 let _guard = OptionsGuard::lock();
797
798 // `c_long` is 32 bits on some platforms, so the cast is not always a
799 // no-op.
800 #[allow(clippy::unnecessary_cast)]
801 let arena_eager_commit = OptionId::ArenaEagerCommit.get() as i64;
802 Self {
803 purge: arena_purge_delay(OptionId::PurgeDelay.get(), OptionId::ArenaPurgeMult.get()),
804 purge_decommits: OptionId::PurgeDecommits.get() != 0,
805 arena_eager_commit,
806 allow_large_os_pages: OptionId::AllowLargeOsPages.get() != 0,
807 transparent_huge_pages: OptionId::AllowThp.get() != 0,
808 arena_reserve: usize::try_from(OptionId::ArenaReserve.get())
809 .unwrap_or(0)
810 .saturating_mul(1024),
811 arena_max_object_size: mi::mi_arena_max_object_size(),
812 }
813 }
814}
815
816/// Process-wide allocator statistics, excluding allocations that C libraries
817/// make through the system allocator.
818///
819/// Operating system accounting (reservations, commits, purges, mappings,
820/// arenas, threads) is current when read. Allocation counters
821/// ([`Self::allocated_bytes`], [`Self::allocated_peak_bytes`],
822/// [`Self::pages`], [`Self::huge_allocations`], [`Self::huge_bytes`]) are
823/// merged from each thread when it collects or exits, but a huge allocation
824/// is counted immediately.
825#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
826pub struct Stats {
827 /// Bytes mimalloc has mapped from the operating system for its arenas,
828 /// page map, and directly mapped pages. Pinned reservations are counted
829 /// in [`Self::pinned_bytes`] instead.
830 pub reserved_bytes: u64,
831 /// Bytes of pinned reservations, which are never purged.
832 pub pinned_bytes: u64,
833 /// Bytes currently committed, as mimalloc accounts them. On systems that
834 /// overcommit, an eagerly committed on-demand arena counts as its slices
835 /// are first used, while an unpinned [`Reserve`] counts in full and then
836 /// again as its slices are first used. On Unix, a purge does not lower
837 /// the figure.
838 pub committed_bytes: u64,
839 /// Peak committed bytes.
840 pub committed_peak_bytes: u64,
841 /// Bytes currently allocated to callers, counted in block sizes (so
842 /// including size-class rounding).
843 pub allocated_bytes: u64,
844 /// Sum of the separately tracked peaks of normal and huge allocations,
845 /// which can exceed their combined peak.
846 pub allocated_peak_bytes: u64,
847 /// Cumulative bytes purged back to the operating system.
848 pub purged_bytes: u64,
849 /// Cumulative purge operations.
850 pub purge_calls: u64,
851 /// Cumulative arena-level purge passes.
852 pub arena_purges: u64,
853 /// Cumulative memory mappings requested from the operating system.
854 pub mmap_calls: u64,
855 /// Cumulative commit operations.
856 pub commit_calls: u64,
857 /// Pages (mimalloc's unit of size-class storage) currently in use.
858 pub pages: u64,
859 /// Threads currently known to the allocator.
860 pub threads: u64,
861 /// Arenas currently managed.
862 pub arenas: u64,
863 /// Cumulative allocations larger than the largest size class, each
864 /// served by a page of its own.
865 pub huge_allocations: u64,
866 /// Cumulative bytes of those allocations.
867 pub huge_bytes: u64,
868}
869
870impl Stats {
871 /// Reads the current statistics, or `None` if the linked mimalloc's
872 /// statistics layout differs in size or version from the vendored header.
873 pub fn read() -> Option<Self> {
874 // SAFETY: `mi_stats_t` holds only integers, and all-zero bits are a
875 // valid integer.
876 let mut raw: mi::mi_stats_t = unsafe { mem::zeroed() };
877 raw.size = size_of::<mi::mi_stats_t>();
878 raw.version = mi::MI_STAT_VERSION;
879 // SAFETY: `raw` is a writable `mi_stats_t` with its size and version
880 // set.
881 if !unsafe { mi::mi_stats_get(&mut raw) } {
882 return None;
883 }
884 Some(Self {
885 reserved_bytes: unsigned(raw.reserved.current),
886 pinned_bytes: PINNED_BYTES.load(Ordering::Relaxed),
887 committed_bytes: unsigned(raw.committed.current),
888 committed_peak_bytes: unsigned(raw.committed.peak),
889 allocated_bytes: unsigned(raw.malloc_normal.current)
890 + unsigned(raw.malloc_huge.current),
891 allocated_peak_bytes: unsigned(raw.malloc_normal.peak) + unsigned(raw.malloc_huge.peak),
892 purged_bytes: unsigned(raw.purged.total),
893 purge_calls: unsigned(raw.purge_calls.total),
894 arena_purges: unsigned(raw.arena_purges.total),
895 mmap_calls: unsigned(raw.mmap_calls.total),
896 commit_calls: unsigned(raw.commit_calls.total),
897 pages: unsigned(raw.pages.current),
898 threads: unsigned(raw.threads.current),
899 arenas: unsigned(raw.arena_count.total),
900 huge_allocations: unsigned(raw.malloc_huge_count.total),
901 huge_bytes: unsigned(raw.malloc_huge.total),
902 })
903 }
904}
905
906/// Converts a statistic to `u64`, reading a negative value as zero. Threads
907/// merge their adjustments into the shared counts at different times, so a
908/// snapshot can catch a count below zero.
909fn unsigned(value: i64) -> u64 {
910 u64::try_from(value).unwrap_or(0)
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916 use std::collections::HashMap;
917
918 const RESERVE_BYTES: usize = 2 * ARENA_CHUNK_SIZE;
919 const PURGE_DELAY: Duration = Duration::from_secs(3600);
920
921 /// A limit between slice boundaries, so the rounding is observable.
922 const MAX_OBJECT: usize = ARENA_CHUNK_SIZE + 1;
923
924 #[global_allocator]
925 static GLOBAL: MiMalloc = MiMalloc::new(Config {
926 purge: Purge::After(PURGE_DELAY),
927 reserve: Some(Reserve::pinned(RESERVE_BYTES)),
928 transparent_huge_pages: Some(false),
929 arena_max_object_size: Some(MAX_OBJECT),
930 ..Config::DEFAULT
931 });
932
933 #[test]
934 fn config_is_applied_before_use() {
935 let status = Status::read();
936 assert!(status.configured);
937 assert_eq!(status.reserve, ReserveOutcome::Pinned);
938 assert_eq!(OptionId::PurgeDelay.get(), 3_600_000);
939 assert_eq!(OptionId::ArenaPurgeMult.get(), 1);
940 assert_eq!(OptionId::AllowThp.get(), 0);
941 let effective = Effective::read();
942 assert_eq!(effective.purge, Some(PURGE_DELAY));
943 assert!(effective.purge_decommits);
944 assert!(!effective.transparent_huge_pages);
945 #[cfg(target_os = "linux")]
946 // SAFETY: this prctl takes integer arguments only.
947 unsafe {
948 assert_eq!(libc::prctl(libc::PR_GET_THP_DISABLE, 0, 0, 0, 0), 1);
949 }
950 assert!(is_global_allocator());
951 assert_eq!(version(), 30503);
952 assert_eq!(good_size(4096), 4096);
953 assert_eq!(good_size(4096 + 32), 5120);
954 }
955
956 #[test]
957 fn purge_delay_is_bounded_and_mirrors_mimalloc() {
958 assert_eq!(purge_millis(Duration::MAX), c_long::from(i32::MAX));
959 assert_eq!(purge_millis(Duration::from_millis(5)), 5);
960 assert_eq!(arena_purge_delay(1000, 4), Some(Duration::from_secs(4)));
961 assert_eq!(arena_purge_delay(0, 4), Some(Duration::ZERO));
962 assert_eq!(arena_purge_delay(-1, 4), None);
963 assert_eq!(arena_purge_delay(1000, -1), None);
964 assert_eq!(
965 arena_purge_delay(c_long::MAX, 2),
966 Some(Duration::from_millis(c_long::MAX as u64))
967 );
968 }
969
970 #[test]
971 fn arena_object_limit_rounds_up_and_is_bounded() {
972 // Bytes round up to KiB, and mimalloc rounds KiB up to a slice.
973 // Oversized values are bounded so their value in bytes fits a `c_long`.
974 assert_eq!(
975 OptionId::ArenaMaxObjectSize.get(),
976 (ARENA_CHUNK_SIZE / 1024 + 1) as c_long
977 );
978 assert_eq!(
979 Effective::read().arena_max_object_size,
980 ARENA_CHUNK_SIZE + ARENA_SLICE_SIZE
981 );
982 assert_eq!(size_option(usize::MAX), c_long::MAX / 1024);
983 }
984
985 #[test]
986 fn constants_match_vendored_headers() {
987 // `mi_option_t` enumerators number from zero in declaration order. The
988 // legacy aliases at the end have explicit values and are skipped.
989 let header = include_str!("../../mimalloc/include/mimalloc.h");
990 let body = header
991 .split_once("typedef enum mi_option_e {")
992 .and_then(|(_, rest)| rest.split_once("} mi_option_t;"))
993 .map(|(body, _)| body)
994 .expect("mimalloc.h must declare the mi_option_t enum");
995 let mut value = -1;
996 let ids: HashMap<&str, i32> = body
997 .lines()
998 .map(|line| {
999 line.split("//")
1000 .next()
1001 .unwrap()
1002 .trim()
1003 .trim_end_matches(',')
1004 })
1005 .filter(|entry| !entry.is_empty() && !entry.contains('='))
1006 .map(|name| {
1007 value += 1;
1008 (name, value)
1009 })
1010 .collect();
1011 for (id, name) in [
1012 (OptionId::ArenaEagerCommit, "mi_option_arena_eager_commit"),
1013 (OptionId::PurgeDecommits, "mi_option_purge_decommits"),
1014 (
1015 OptionId::AllowLargeOsPages,
1016 "mi_option_allow_large_os_pages",
1017 ),
1018 (OptionId::PurgeDelay, "mi_option_purge_delay"),
1019 (OptionId::ArenaReserve, "mi_option_arena_reserve"),
1020 (OptionId::ArenaPurgeMult, "mi_option_arena_purge_mult"),
1021 (OptionId::AllowThp, "mi_option_allow_thp"),
1022 (
1023 OptionId::ArenaMaxObjectSize,
1024 "mi_option_arena_max_object_size",
1025 ),
1026 ] {
1027 assert_eq!(
1028 ids.get(name),
1029 Some(&(id as i32)),
1030 "{name} must carry its vendored mi_option_t value"
1031 );
1032 }
1033
1034 let types = include_str!("../../mimalloc/include/mimalloc/types.h");
1035 let max_align: usize = types
1036 .lines()
1037 .find_map(|line| line.strip_prefix("#define MI_MAX_ALIGN_SIZE"))
1038 .and_then(|rest| rest.split_whitespace().next())
1039 .and_then(|value| value.parse().ok())
1040 .expect("types.h must define MI_MAX_ALIGN_SIZE as an integer");
1041 assert_eq!(max_align, MALLOC_ALIGNMENT);
1042 }
1043
1044 #[test]
1045 fn stats_layout_matches_vendored_header() {
1046 // `mi_stats_t` is mirrored field by field, so its layout must follow
1047 // the `MI_STAT_FIELDS` list, the counter shapes, the bin counts, and
1048 // the version in `mimalloc-stats.h`. mimalloc refuses to fill a
1049 // mismatched size or version at runtime, but a reordering that keeps
1050 // the size would read wrong values.
1051 let header = include_str!("../../mimalloc/include/mimalloc-stats.h");
1052
1053 // The version and the last size bin index must match their defines.
1054 let define = |name: &str| -> usize {
1055 header
1056 .lines()
1057 .find_map(|line| line.strip_prefix(&format!("#define {name}")))
1058 .and_then(|rest| rest.split_whitespace().next())
1059 .map(|value| value.trim_matches(|c| c == '(' || c == ')' || c == 'U'))
1060 .and_then(|value| value.parse().ok())
1061 .unwrap_or_else(|| panic!("mimalloc-stats.h must define {name} as an integer"))
1062 };
1063 assert_eq!(define("MI_STAT_VERSION"), mi::MI_STAT_VERSION);
1064 assert_eq!(define("MI_BIN_HUGE"), mi::MI_BIN_HUGE);
1065
1066 // Chunk bins are the enumerators declared before `MI_CBIN_COUNT`.
1067 let chunk_bins = header
1068 .split_once("typedef enum mi_chunkbin_e {")
1069 .and_then(|(_, rest)| rest.split_once("MI_CBIN_COUNT"))
1070 .map(|(body, _)| body.matches("MI_CBIN_").count())
1071 .expect("mimalloc-stats.h must declare the mi_chunkbin_e enum");
1072 assert_eq!(chunk_bins, mi::MI_CBIN_COUNT);
1073
1074 // `MI_STAT_FIELDS` must list the mirrored statistics in order. Its
1075 // body is the run of lines that end with a backslash.
1076 let fields: Vec<(&str, &str)> = header
1077 .lines()
1078 .skip_while(|line| !line.starts_with("#define MI_STAT_FIELDS()"))
1079 .skip(1)
1080 .take_while(|line| line.trim_end().ends_with('\\'))
1081 .filter_map(|line| {
1082 let entry = line.trim().trim_end_matches('\\').trim();
1083 let entry = entry.split("/*").next().unwrap().trim();
1084 let (kind, rest) = entry.split_once('(')?;
1085 let name = rest.split_once(')')?.0.trim();
1086 let kind = match kind.trim() {
1087 "MI_STAT_COUNT" => "mi_stat_count_t",
1088 "MI_STAT_COUNTER" => "mi_stat_counter_t",
1089 other => panic!(
1090 "MI_STAT_FIELDS entry {other} must be MI_STAT_COUNT or MI_STAT_COUNTER"
1091 ),
1092 };
1093 Some((kind, name))
1094 })
1095 .collect();
1096 assert_eq!(fields, mi::STAT_FIELDS);
1097
1098 // The counter structs must have the mirrored members in order, and
1099 // the statistics struct must declare each mirrored array.
1100 let body = |name: &str| -> &str {
1101 header
1102 .split_once(&format!("typedef struct {name}"))
1103 .and_then(|(_, rest)| rest.split_once('{'))
1104 .and_then(|(_, rest)| rest.split_once('}'))
1105 .map(|(body, _)| body)
1106 .unwrap_or_else(|| panic!("mimalloc-stats.h must declare {name}"))
1107 };
1108 let members = |body: &str| -> Vec<String> {
1109 body.lines()
1110 .filter_map(|line| line.trim().split_once(';'))
1111 .map(|(declaration, _)| {
1112 declaration.split_whitespace().collect::<Vec<_>>().join(" ")
1113 })
1114 .collect()
1115 };
1116 assert_eq!(
1117 members(body("mi_stat_count_s")),
1118 ["int64_t total", "int64_t peak", "int64_t current"]
1119 );
1120 assert_eq!(members(body("mi_stat_counter_s")), ["int64_t total"]);
1121 let stats = body("mi_stats_s");
1122 for array in [
1123 "mi_stat_count_t _stat_reserved[4]",
1124 "mi_stat_counter_t _stat_counter_reserved[4]",
1125 "mi_stat_count_t malloc_bins[MI_BIN_HUGE+1]",
1126 "mi_stat_count_t page_bins[MI_BIN_HUGE+1]",
1127 "mi_stat_count_t chunk_bins[MI_CBIN_COUNT]",
1128 ] {
1129 assert!(
1130 members(stats).iter().any(|member| member == array),
1131 "mimalloc-stats.h must declare {array}"
1132 );
1133 }
1134
1135 // Only `size` and `version` precede the first statistic, and the
1136 // header and the mirror both align it to 8 bytes.
1137 let (header_fields, _) = stats
1138 .split_once("mi_decl_align(8)")
1139 .expect("mimalloc-stats.h must align the first statistic to 8 bytes");
1140 assert_eq!(members(header_fields), ["size_t size", "size_t version"]);
1141 assert_eq!(align_of::<mi::mi_stats_t>(), 8);
1142 }
1143
1144 #[test]
1145 fn fast_paths_hand_out_aligned_memory() {
1146 // Both branches of every entry point return aligned memory and free
1147 // each other's blocks. Each layout keeps several blocks live because
1148 // consecutive blocks of a word-multiple size class alternate between
1149 // word and 16-byte alignment, so one block can be aligned by chance.
1150 for (size, align) in [
1151 (100usize, 1usize),
1152 (100, 8),
1153 (100, 16),
1154 (1, 16),
1155 (8, 16),
1156 (16, 16),
1157 (24, 16),
1158 (32, 16),
1159 (48, 16),
1160 (3, 8),
1161 (100, 64),
1162 (4096, 4096),
1163 (100, 4096),
1164 ] {
1165 let layout = Layout::from_size_align(size, align).expect("layout must be valid");
1166 // SAFETY: valid non-zero layouts, and every pointer is freed below.
1167 unsafe {
1168 let mut live = Vec::new();
1169 for _ in 0..16 {
1170 let a = GLOBAL.alloc(layout);
1171 assert!(
1172 !a.is_null() && (a as usize).is_multiple_of(align),
1173 "the pointer must be non-null and aligned for {layout:?}"
1174 );
1175 let z = GLOBAL.alloc_zeroed(layout);
1176 assert!(
1177 !z.is_null() && (z as usize).is_multiple_of(align),
1178 "the pointer must be non-null and aligned for {layout:?}"
1179 );
1180 assert!(std::slice::from_raw_parts(z, size).iter().all(|b| *b == 0));
1181 live.push((a, z));
1182 }
1183 let grown = Layout::from_size_align(10_000, align).expect("layout must be valid");
1184 for (a, z) in live {
1185 let r = GLOBAL.realloc(a, layout, grown.size());
1186 assert!(
1187 !r.is_null() && (r as usize).is_multiple_of(align),
1188 "the pointer must be non-null and aligned for {layout:?}"
1189 );
1190 GLOBAL.dealloc(r, grown);
1191 GLOBAL.dealloc(z, layout);
1192 }
1193 }
1194 }
1195 }
1196
1197 #[test]
1198 fn realloc_shrink_keeps_alignment() {
1199 // Shrinking a 16-byte aligned block to 8 bytes must use the aligned
1200 // path, because blocks of the 8-byte class are 16-byte aligned only
1201 // every other block.
1202 let layout = Layout::from_size_align(48, 16).expect("layout must be valid");
1203 let shrunk = Layout::from_size_align(8, 16).expect("layout must be valid");
1204 // SAFETY: valid non-zero layouts, and every pointer is freed below.
1205 unsafe {
1206 let live: Vec<*mut u8> = (0..16).map(|_| GLOBAL.alloc(layout)).collect();
1207 for p in live {
1208 let r = GLOBAL.realloc(p, layout, shrunk.size());
1209 assert!(
1210 !r.is_null() && (r as usize).is_multiple_of(16),
1211 "the pointer must be non-null and aligned for {shrunk:?}"
1212 );
1213 GLOBAL.dealloc(r, shrunk);
1214 }
1215 }
1216 }
1217
1218 #[test]
1219 fn realloc_grows_into_the_exact_size_class() {
1220 // Growing a 16-byte aligned block to a multiple of 16 takes the plain
1221 // path, so the moved block is in its exact size class and must move
1222 // again to grow by 16 bytes. A block padded by the aligned path would
1223 // have room and stay in place.
1224 let layout = Layout::from_size_align(4096, 16).expect("layout must be valid");
1225 let grown = Layout::from_size_align(8192, 16).expect("layout must be valid");
1226 let widened = Layout::from_size_align(8192 + 16, 16).expect("layout must be valid");
1227 // SAFETY: each pointer came from GLOBAL with the layout it was last
1228 // allocated or resized to, and the block is freed below.
1229 unsafe {
1230 let p = GLOBAL.realloc(GLOBAL.alloc(layout), layout, grown.size());
1231 assert!(!p.is_null() && p.addr().is_multiple_of(16));
1232 let q = GLOBAL.realloc(p, grown, widened.size());
1233 assert!(!q.is_null() && q.addr().is_multiple_of(16));
1234 assert_ne!(q, p, "a block of its exact size class must move to grow");
1235 GLOBAL.dealloc(q, widened);
1236 }
1237 }
1238
1239 #[test]
1240 fn stats_reflect_reservation_and_huge_allocations() {
1241 let before = Stats::read().expect("stats must be available");
1242
1243 // The pinned reservation serves the tests' small working set, so
1244 // mimalloc maps only its page map, far less than an on-demand arena.
1245 // Another test adds a second pinned arena, so the count may exceed one.
1246 assert!(before.pinned_bytes >= RESERVE_BYTES as u64);
1247 assert!(before.arenas >= 1);
1248 assert!(before.reserved_bytes < RESERVE_BYTES as u64);
1249 assert!(before.committed_bytes > 0);
1250 assert!(before.threads >= 1);
1251
1252 let largest = vec![1u8; LARGE_OBJECT_LIMIT];
1253 let after = Stats::read().expect("stats must be available");
1254 assert_eq!(after.huge_allocations, before.huge_allocations);
1255 drop(largest);
1256
1257 let size = LARGE_OBJECT_LIMIT + 1;
1258 let block = vec![1u8; size];
1259 let after = Stats::read().expect("stats must be available");
1260 assert!(after.huge_allocations > before.huge_allocations);
1261 assert!(after.huge_bytes >= before.huge_bytes + size as u64);
1262 assert!(after.allocated_bytes >= before.allocated_bytes + size as u64);
1263 drop(block);
1264 let released = Stats::read().expect("stats must be available");
1265 assert!(released.allocated_bytes < after.allocated_bytes);
1266 }
1267
1268 #[test]
1269 fn reservations_are_trimmed_to_chunks() {
1270 let chunk = mi::mi_arena_min_size();
1271 assert_eq!(chunk, ARENA_CHUNK_SIZE);
1272 assert_eq!(whole_chunks(chunk - 1), None);
1273 assert_eq!(whole_chunks(chunk + chunk / 2), Some(chunk));
1274 assert_eq!(whole_chunks(usize::MAX), Some(usize::MAX / chunk * chunk));
1275 let before = PINNED_BYTES.load(Ordering::Relaxed);
1276 assert_eq!(manage_pinned(chunk, true), ReserveOutcome::Pinned);
1277 assert_eq!(PINNED_BYTES.load(Ordering::Relaxed) - before, chunk as u64);
1278 }
1279
1280 #[test]
1281 fn stats_clamp_negative_counts() {
1282 // A negative count reads as zero, and a positive one is unchanged.
1283 assert_eq!(unsigned(-1), 0);
1284 assert_eq!(unsigned(i64::MAX), i64::MAX as u64);
1285 }
1286
1287 #[test]
1288 fn smallest_size_class_pages_stay_within_mimalloc_block_counts() {
1289 // mimalloc counts a page's blocks in 16-bit fields. With
1290 // `large-objects-4mb`, a 512 KiB page of 8-byte blocks stays within
1291 // that limit only because the page header takes a few blocks. Fill more
1292 // than one such page and check that every block is distinct and no page
1293 // holds more blocks than it can count.
1294 const COUNT: usize = u16::MAX as usize + 4096;
1295 let boxes: Vec<Box<u8>> = (0..COUNT).map(|i| Box::new(i as u8)).collect();
1296 let mut addresses: Vec<usize> = boxes.iter().map(|b| ptr::from_ref(&**b).addr()).collect();
1297 addresses.sort_unstable();
1298 addresses.dedup();
1299 assert_eq!(addresses.len(), COUNT);
1300 let mut blocks_per_page: HashMap<usize, usize> = HashMap::new();
1301 for address in &addresses {
1302 *blocks_per_page
1303 .entry(address / ARENA_SLICE_SIZE)
1304 .or_default() += 1;
1305 }
1306 assert!(
1307 blocks_per_page
1308 .values()
1309 .all(|count| *count <= u16::MAX as usize)
1310 );
1311 assert!(boxes.iter().enumerate().all(|(i, b)| **b == i as u8));
1312 }
1313
1314 #[test]
1315 fn unpinned_and_default_configs_are_constructible() {
1316 let defaulted = MiMalloc::new(Config::DEFAULT);
1317 assert_eq!(defaulted.config(), &Config::default());
1318 assert!(!Reserve::unpinned(1).pinned);
1319 }
1320
1321 #[test]
1322 fn configuration_is_process_wide() {
1323 // Allocating through a second instance leaves the process state and
1324 // the options GLOBAL applied unchanged.
1325 let other = MiMalloc::new(Config {
1326 purge: Purge::Never,
1327 ..Config::DEFAULT
1328 });
1329 let before = Status::read();
1330 assert!(before.configured);
1331 let layout = Layout::new::<u64>();
1332 // SAFETY: a valid non-zero layout, and the block is freed below.
1333 unsafe {
1334 let block = other.alloc(layout);
1335 assert!(!block.is_null());
1336 other.dealloc(block, layout);
1337 }
1338 assert_eq!(Status::read(), before);
1339 assert_eq!(OptionId::PurgeDelay.get(), 3_600_000);
1340 }
1341}