Frontend Labs
⋯

43. Memory: mmap and a counting global allocator

43.1 Memory-mapped files

use memmap2::Mmap;
use std::fs::File;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let path = std::env::temp_dir().join("rust-guide-mmap.log");
    let mut f = File::create(&path)?;
    for i in 0..100_000 { writeln!(f, "event {i} status={}", if i % 97 == 0 { 500 } else { 200 })?; }
    drop(f);

    let file = File::open(&path)?;
    // SAFETY: the mapping is read-only, and no other process truncates this file while it is mapped.
    // If one did, reading the vanished pages would raise SIGBUS. That is why Mmap::map is `unsafe`.
    let map = unsafe { Mmap::map(&file)? };
    let bytes: &[u8] = &map;                       // the file, as a byte slice, with no read() copies

    let lines = bytes.iter().filter(|&&b| b == b'\n').count();
    let errors = bytes.split(|&b| b == b'\n').filter(|l| l.ends_with(b"500")).count();
    println!("{lines} lines, {errors} errors, {} bytes mapped", map.len());
    std::fs::remove_file(path)
}

Pages are loaded lazily through page faults (OS Ch 11), so mapping a 10 GB log costs nothing until you touch it. Note the SAFETY comment: Rust's &[u8] promises the bytes will not change, but the kernel lets another process change or truncate the underlying file. That is the reason Mmap::map is unsafe. C §57 covers msync and shared writable mappings.

43.2 A counting global allocator

Every Box, Vec, and String allocates through one global allocator, which you can replace. Wrapping the system allocator to count allocations is a cheap, always-on way to find allocation-heavy code paths:

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static BYTES: AtomicUsize = AtomicUsize::new(0);

// SAFETY: we forward to System, which upholds GlobalAlloc's contract; we only add counters.
unsafe impl GlobalAlloc for Counting {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        ALLOCS.fetch_add(1, Relaxed);
        BYTES.fetch_add(layout.size(), Relaxed);
        unsafe { System.alloc(layout) }
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        unsafe { System.dealloc(ptr, layout) }
    }
}

#[global_allocator]
static GLOBAL: Counting = Counting;

fn measure<T>(label: &str, f: impl FnOnce() -> T) -> T {
    let (a0, b0) = (ALLOCS.load(Relaxed), BYTES.load(Relaxed));
    let out = f();
    println!("{label:<28} {:>6} allocs {:>9} bytes", ALLOCS.load(Relaxed) - a0, BYTES.load(Relaxed) - b0);
    out
}

fn main() {
    let words: Vec<String> = (0..10_000).map(|i| format!("w{i}")).collect();
    measure("join via format! in a loop", || {
        let mut s = String::new();
        for w in &words { s = format!("{s}{w},"); } // allocates a new String every iteration
        s.len()
    });
    measure("push_str with capacity", || {
        let mut s = String::with_capacity(words.iter().map(|w| w.len() + 1).sum());
        for w in &words { s.push_str(w); s.push(','); }
        s.len()
    });
}
join via format! in a loop    19998 allocs 868126828 bytes
push_str with capacity            1 allocs     58890 bytes

The first version copies the whole string on every iteration. It allocated 868 MB in total, with quadratic growth, to build a 58 KB result. This is the same bug as s += x in a Python or Java loop, and the counting allocator makes it obvious.

The same #[global_allocator] line is how you switch to mimalloc or jemalloc in production (static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;). The C guide's Ch 39 explains what such an allocator does internally.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36