Repository navigation
v0.9.0 — Real GlobalAlloc Implementation
Pre-releasemod-alloc v0.9.0 — Release Notes
Date: 2026-05-13
Compare: v0.1.0...v0.9.0
Headline
Transforms mod-alloc from a name-claim placeholder into a working
global-allocator wrapper. Installing ModAlloc as
#[global_allocator] now tracks every allocation, deallocation,
reallocation, and zero-init allocation against four lock-free
AtomicU64 counters. Measured per-allocation overhead is well
under the 50 ns target set by REPS section 6.
What's new
Public API: real implementations behind the existing surface
unsafe impl GlobalAlloc for ModAlloc— the four hot-path
methods (alloc,alloc_zeroed,dealloc,realloc) now
delegate tostd::alloc::Systemfor the underlying allocation
and record the event in atomic counters. Everyunsafeblock
carries a SAFETY comment per.dev/DIRECTIVES.mdsection 3.ModAlloc::snapshot— now returns the running counter values
from the liveGlobalAllocpath. Inv0.1.0it always returned
zeros.ModAlloc::reset— zeroes the counters atomically. Documented
caveat: resetting while live allocations exist can cause
current_bytesto wrap on subsequent deallocations; reset before
the workload begins for clean accounting.Profiler::start/Profiler::stop— wired against a
process-wideAtomicPtr<ModAlloc>handle that theGlobalAlloc
impl populates lazily on first call. No explicit registration
step is required. The signature ofProfiler::start() -> Selfis
preserved exactly as theREPSAPI surface specifies.
Profiler::stop returns deltas for alloc_count, total_bytes,
and current_bytes. peak_bytes is the absolute high-water mark
observed during the profiling window (peak-delta has no meaningful
semantic). The rustdoc on Profiler::stop calls this out
explicitly so callers do not misread peak_bytes.
Counter strategy
| Counter | Updated on alloc |
Updated on dealloc |
|---|---|---|
alloc_count |
+= 1 |
(unchanged) |
total_bytes |
+= size |
(unchanged) |
current_bytes |
+= size |
-= size |
peak_bytes |
fetch_max(current_after_add) |
(unchanged) |
All updates use Ordering::Relaxed. peak_bytes uses
AtomicU64::fetch_max (stable since Rust 1.45). realloc is
counted as one allocation event; total_bytes increases by the
growth delta on a growing realloc and is unchanged on a shrinking
realloc, which preserves the invariant peak_bytes ≤ total_bytes.
Reentrancy guard
The allocator hook is recursion-safe. A thread-local
Cell<bool> flag (initialised via const { Cell::new(false) } so
the TLS access path itself does not allocate) is set on entry to
the tracking path. Any allocation transitively triggered from
inside the hook sees the flag and bypasses tracking, forwarding
straight to System. A RAII guard struct clears the flag on drop
(including on panic).
Test additions
tests/ gains four new integration tests, each compiled as its
own binary so each can own its #[global_allocator] without
conflicting with the others:
counters_accuracy.rs— single-thread counter correctness.
Asserts thatalloc_countandtotal_bytesmove by at least
the workload's expected amount and thatpeak_bytesnever
decreases after a drop.concurrent_alloc.rs— multi-thread atomic-contention
stress test. 64 worker threads × 5,000 allocations each (320,000
total). Asserts the aggregate counter movement matches the
workload within sanity bounds and that the run terminates within
a generous timeout (no deadlock).profiler_delta.rs— Profiler delta math. Asserts the
returned delta covers the in-window workload and that
peak_bytes(absolute) is at leastcurrent_bytes(delta).reentrancy.rs— reentrancy-guard smoke test. Allocation-
heavy workload with formatting paths andDropimpls that
allocate. A broken guard would stack-overflow; reaching the end
proves the invariant holds.
The existing tests/smoke.rs is preserved unchanged.
src/lib.rs #[cfg(test)] mod tests adds 7 new unit tests
covering the private record_alloc / record_dealloc /
record_realloc helpers and the ReentryGuard::enter nesting
semantics, alongside the existing 3 placeholder smoke checks.
Example additions
examples/basic.rs— rewritten to installModAllocas
#[global_allocator]and print real counter values for both the
profiler delta and the process-wide snapshot.examples/bench_overhead.rs— per-allocation overhead
micro-benchmark. Runs 1,000,000 fixed-size allocate/deallocate
cycles and reports average nanoseconds per cycle. Sanity-check
only; criterion is banned by the zero-dep policy and
#[bench]is unstable.
Measured on the development workstation: 34.9 ns per
alloc + dealloc cycle end-to-end (which includes the cost of
System itself). The crate's overhead on top of System is a
small fraction of that figure, well under the 50 ns target from
REPS section 6.
Design notes
Per-thread arena deferred to v0.9.1
The original ROADMAP entry for v0.9.0 envisaged a 64 KB
per-thread arena with periodic global aggregation. This release
ships with direct atomic increments on four shared counters
instead. Rationale:
- For four
u64counters, the indirection of per-thread buffering
does not pay off. The bench above shows the <50 ns target is
achievable with direct atomics. - Per-thread buffering becomes load-bearing in
v0.9.1when
backtrace state (32-64 bytes per allocation) would otherwise
serialise on the global path. Introducing it then is the right
point in the line.
Full rationale: .dev/DESIGN.md section 2.
Profiler lookup via lazy AtomicPtr
Profiler::start() cannot take a &'static ModAlloc argument
without changing the REPS-frozen signature. Rather than require
an explicit registration call, the GlobalAlloc::alloc impl
records self into a process-wide AtomicPtr<ModAlloc> on its
first non-reentrant call. Profiler reads from that handle. Cost
on the hot path: one Relaxed load per alloc call (essentially
free) plus a one-time compare_exchange on the very first call.
Feature flags
backtraces and dhat-compat remain defined in Cargo.toml so
build matrices stay green and downstream callers can opt in once
the features ship. In v0.9.0 both compile as no-ops. Real
implementations land in v0.9.1 (Tier 2: inline backtrace
capture) and v0.9.3 (Tier 3: DHAT-compatible JSON output).
Migration
The public API surface is unchanged. Callers using the v0.1.0
placeholder API (ModAlloc::new, snapshot, reset,
Profiler::start, Profiler::stop, AllocStats) continue to
compile and run. Behavioural changes:
- Callers that did not install
ModAllocas
#[global_allocator]see no change.snapshotstill returns
zeros from a freshModAlloc::new()instance because no
allocations are recorded against it. - Callers that did install it now see real counter values
wherev0.1.0returned zeros.
No code edits required.
Verification
Full matrix run on Windows host:
cargo build(default features)cargo build --no-default-featurescargo build --features counterscargo build --features dhat-compat(compiles; feature is a
no-op inv0.9.0as documented)cargo build --all-featurescargo +1.75 build --all-features(MSRV)cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo test --all-features: 28/28 pass (10 unit + 4 new
integration + 5 smoke + 9 doctests)- Banned-word scan (
comprehensive,robust,seamless,
leverage): 0 hits across shipping files - Em-dash scan: 0 hits across shipping files
Downstream impact
dev-benchcurrently usesdhat 0.3for alloc-tracking and is
pinned at MSRV 1.85 by that dependency chain
(backtrace 0.3.76→addr2line 0.25.1). Withv0.9.0shipping
Tier 1 counters,dev-benchcan begin its swap and the MSRV
unblock for the widerdev-*family becomes possible. Full
drop-in replacement ofdhatlands once Tier 2 (backtraces) and
Tier 3 (DHAT-compat output) ship inv0.9.1/v0.9.3.- Strategy context:
C:\Dev\libraries\Rust\_strategy\MSRV_AUDIT_DEV_LIBS.md.
Limitations
reset()underflow. Callingreset()while allocations are
outstanding zeroescurrent_bytes; subsequent deallocations
thenfetch_subfrom zero and wrap. Documented in the rustdoc.
Reset before the workload begins.backtracesanddhat-compatare no-ops. Both features
compile but produce no functional change. Tier 2 lands in
v0.9.1; Tier 3 inv0.9.3.total_bytessemantics forrealloc. This release uses
growth-delta accounting (a growing realloc adds the delta, a
shrinking realloc adds zero). DHAT counts realloc differently
(the full new size each time).v0.9.3will revisit this when
dhat-compatJSON output ships; the in-memory counter semantics
may stay as they are with a separate translation layer at
serialisation time.AtomicPtrregistration is first-call-wins. If two
ModAllocinstances ever existed (one as#[global_allocator],
another constructed in user code),Profilerwould lock onto
whichever calledalloc()first. In practice this is always
the#[global_allocator]instance, so the failure mode is
theoretical.
Release ceremony
Standard pattern:
git tag -a v0.9.0 -m "Release v0.9.0 - real GlobalAlloc implementation"git push origin maingit push origin v0.9.0cargo publish --dry-run --all-featurescargo publish- Confirm
https://crates.io/crates/mod-alloc/0.9.0is live.
GitHub release title: v0.9.0 - real GlobalAlloc implementation.
Tag as pre-release; 1.0.0 is the API-stability handshake and
comes only after v0.9.4 (dev-bench integration) ships and bakes.
Full Changelog: v0.1.0...v0.9.0