Skip to main content

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}