awh::alloc
A memory allocator that replaces the system one entirely, for the whole process. Not a wrapper around malloc and not a pool for special cases: once it takes over, every allocation goes through it — ours, the standard library's and those of any third-party library linked into the program.
Four needs the system allocator does not cover
Accounting
How much application code holds right now, what the peak was, how much sits in caches, how much went back to the system. The system allocator is either silent or reports on the whole process.
Returning memory
A server that releases pages in the middle of a traffic spike pays in latency. The policy should be set by whoever writes the application, not by another library's defaults.
Fault analysis
When a program crashes you need to know: null, past the end of a block, use after free, or foreign memory. The system allocator can't answer at all.
Protecting secrets
Keys and passwords need memory that stays out of crash dumps, never swaps and is wiped on release. That is a property of allocation and cannot be added from outside.
All of it through calls, not environment variables: the application sets the mode itself and can change it at runtime.
Three capture techniques, one per family of systems
| Technique | Systems | How it works |
|---|---|---|
| Symbol interposition | Linux, all BSDs, Solaris, illumos | our object defines malloc and the linker puts it in place of the system one — captured before the first line of main |
| Symbols plus zone | macOS | interposition redirects the image's own calls, and a malloc_zone_t catches allocations made inside libsystem |
| Entry patching | Windows | the entry points of ucrtbase functions are rewritten to jump to ours; aligned allocation is caught separately |
Proven by a decisive observation: memory allocated inside the standard library (for example by std::string) is found in our region. A pool plugged into your own code cannot do that.
Five layers, with features alongside
| Layer | What it does |
|---|---|
| Capture | malloc, free, calloc, realloc, aligned allocation |
| Cache | per-thread cache, takes no lock at all |
| Central | central free lists, one lock per size class — exchange in batches |
| Pages | page heap — exchange in spans |
| Source | pages from the system, replaceable by the consumer |
- 62 size classes from 16 bytes to 32,768, rounding loss no more than an eighth of a block.
- 8 KB heap pages, 4 MB chunks aligned to their size: a chunk's start is found with one mask.
- Free-list links live inside the blocks. Pointers are mangled with a per-process seed: overwriting a free block does not give an attacker a controlled pointer.
- Fast path — a dozen instructions and no lock: thread cache, size class by table lookup without division, block from the list head, per-thread accounting.
Beside the ladder: Huge for large allocations, Guard for guard pages, Profile for allocation sites, Trace for fault addresses.
What the system allocator simply doesn't have
- Fault address analysis resolve() — null page, foreign memory, live block, freed block, past the end, before the start.
- Guard pages guardRate — sampled blocks behind closed pages: an overrun faults on the spot. Your own ASan, working in release builds.
- Use-after-free detection — quarantine keeps freed memory filled with a pattern; spoiled() reports the number of corrupted blocks, the address and the offset.
- Allocation-site profiling profileRate with stack capture — find leaks in a running program.
- Secrets store secure() — memory kept out of dumps, locked against swap and wiped on release. shelter_t tells you what actually took effect.
- Release policy — never, on request, by threshold on free, or by a service thread after a delay.
- Usage queries — six figures: in use, peak, taken from the system, free in caches, free in the heap, returned.
- Ceiling heapLimit guards everything taken from the system, not just the heap.
alloc::options_t options; options.purge = alloc::purge_t::ONFREE; // never · on request · by threshold · by thread options.heapLimit = 2ull * 1024 * 1024 * 1024; // ceiling on everything taken from the system options.hugeCache = 16 * 1024 * 1024; // keep large regions for reuse options.guardRate = 1000; // one block in a thousand behind guard pages if(!alloc::Allocator::capture(options)) /* capture did not happen */;
Mixed sizes — the way real applications work
Nanoseconds per operation, lower is better; sizes spread across all size classes.
| System | AWH | system | jemalloc | tcmalloc |
|---|---|---|---|---|
| macOS ARM64 | 2.81 | 11.27 | 10.73 | — |
| Debian x86-64 | 10.04 | 22.24 | 42.56 | 8.45 |
| DragonFly BSD | 11.09 | 26.90 | 650.10 | 9.16 |
| Alpine (musl) | 11.19 | 1,116.07 | 50.98 | 8.61 |
| NetBSD | 11.31 | 34.96 | 35.06 | — |
| FreeBSD | 11.84 | 40.31 | (is the system one) | 9.02 |
| OpenBSD | 21.92 | 2,563.45 | — | — |
jemalloc beaten everywhere
By 3.1–3.9× on the mixed workload, 58× on DragonFly.
tcmalloc ahead by 7–15 %
It stores the size class in the block header; we identify a block by its address — which is what makes fault analysis possible.
Evenness — 22 %
Spread across thirteen systems is 10.1–12.4 ns; competitors vary by orders of magnitude.
In-place growth and retention of large regions
realloc grows in place
A region grows into neighbouring free pages and keeps its address. A doubling chain from 16 bytes to 256 KB: was 14,750 µs, now 3,259 against 2,634 for the system allocator.
Keeping large regions
Above 4 MB the cost lies in first-touching pages. Freed regions are kept under the hugeCache ceiling: “allocate and fill” 5 MB went from 15,761 to 108,799 MB/s against 110,000 for the system.
Turning a feature off returns its memory at once: zero in quarantine releases everything the quarantine held, without waiting for the next free.
Eight systems, a fuzzer and sanitizers
| System | Suite | TSan |
|---|---|---|
| macOS 26 | 63/64 | clean |
| Debian 12 · Alpine · FreeBSD 15 | 49/49 | clean |
| NetBSD 10.1 | 49/49 | — |
| OpenBSD 7.9 · Solaris 11.4 | 48/49 | — |
| Windows 11 ARM64 | passed | — |
| Elbrus-8C2 (lcc) | port in progress | — |
Line coverage of the module is 79.65 %, and the uncovered part is explained: allocator teardown can't be tested inside the process, fork paths are exercised only on ELF systems, defensive branches are unreachable by design.
Skips are recorded system properties: OpenBSD has no syscall(2), and Sun systems don't support calling it directly.
Under sanitizers the capture does not happen, and the suite asserts that; the layers are tested under them directly.