QuicxDocs

apool v2 Allocator

apool v2 hands 16 KiB pages to the block sizes that are actually arriving and takes them back when they empty. The same 8 MiB that held 655 tasks of 100 B under the old fixed split now holds 46,593, and gives its memory back to the OS after a burst.

Quicx 2.0 keeps its promise — one budget, mapped once, never exceeded — and changes what happens inside it. Up to 1.0.4 the budget was split between block sizes by fixed percentages (PMAD), decided before the first task arrived. apool lets the traffic decide instead: the diagram below follows one page, page 4, through every part of it.

Architecture#

apool v2 allocator diagram: one mapping of 508 pages of 16 KiB followed by 32-byte page records; size classes own lists of pages; freed blocks inside a page form an encoded free list; empty pages stay warm, then give their memory back to the OS.
One mapping
The budget is one mmap at startup, never grown or moved: 508 pages of 16 KiB, then one 32-byte record per page. Pages come first, so every page starts on an OS page boundary and can be given back.
No block headers
A block’s page is (address − start) / 16 KiB, and that page’s record holds everything else: the size it serves, how many blocks are live, its free list and its bump offset. A 100 B request costs a 112 B block, nothing more.
Pages per size
68 sizes, 16 B to 1,088 B in steps of 16. A size takes a free page only when it has no room left, and a page whose last block is freed goes back to a shared list where any size can take it next. Live blocks never move.
Inside a page
Freed blocks form a list through their own first 8 bytes; blocks never used yet are handed out from a bump offset. Both are O(1).
One block per task
A task is the 64 B Task record with its payload right behind it — one allocation, one free, freed the moment MSG_TASK is sent. Connections and workers come from a separate 3-page control pool, so a full task pool can never lock a worker out.

Ideas from the literature#

Every idea in apool v2 comes from a published allocator and sits behind its own switch, so its effect could be measured on its own. Each one can be turned on or off in the [allocator] section of quicx.conf.

FeatureIdea fromWhat it buysDefault
Pages move between sizesslab allocator (Bonwick 1994), mimalloc, memcached46,593 tasks of 100 B instead of 655; follows size changes without a restartalways
ReserveLinux zone watermarks, memcachedafter a flood of one size, a new size still gets room (16 tasks of 900 B instead of 0); costs 1.4 % of capacityon
Hardeningmimalloc secure modefree-list links XOR-encoded with a per-pool key; double frees and overwritten links are caught; about 1 ns per task (M4 Pro)on
Purge with a warm listjemalloc, mimallocbeyond 63 empty pages the oldest gives its memory back to the OS; 1,104 KB held after a drain instead of 8,096 KBon
Lifetime classesLLAMA (Maas et al. 2020)long-lived blocks get their own pages; 7,648 instead of 16 tasks fit after a pinning pattern — when a caller passes a hintavailable, unused
Fullest page firstHoard (Berger et al. 2000)measured: no gain on queue traffic, 5 ns slower per taskoff

Benchmarks#

The allocator on its own, with the same workloads on PMAD, the first adaptive pool and apool v2, on the default 8 MiB. Capacity and the queue depths are identical on the Apple M4 Pro and the Intel i3-2100; the time per task is the median of 5 runs.

PMAD (1.0.4)apool v1apool v2
100 B tasks that fit65547,45046,872
900 B after a flood of 40 B180016
900 B with 1 in 20 tasks pinned18007,648
deepest mixed FIFO queue5,31230,72029,952
ns per task, M4 Pro13.324.914.8
ns per task, i3-210043.3103.372.8

Settings#

The defaults are the measured best: 16 KiB pages (4–64 KiB were compared), 63 warm pages and 7 reserved pages at the default budget. warm_pages and reserve_pages in quicx.conf override the last two; the page size is fixed at build time.