{"id":51573114,"url":"https://github.com/czhao-dev/thread-local-allocator","last_synced_at":"2026-07-10T21:30:35.930Z","repository":{"id":370737989,"uuid":"1281570753","full_name":"czhao-dev/thread-local-allocator","owner":"czhao-dev","description":"Thread-local memory allocator in C11: per-thread cache (tcmalloc-style), slab allocator, and boundary-tag free list with O(1) coalescing","archived":false,"fork":false,"pushed_at":"2026-07-10T15:24:54.000Z","size":336,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-10T17:10:36.669Z","etag":null,"topics":["allocator","benchmarking","c","c11","concurrency","free-list","malloc","memory-allocator","mmap","slab-allocator","systems-programming","tcmalloc","thread-local-storage"],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/czhao-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-26T17:32:27.000Z","updated_at":"2026-07-10T16:04:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/czhao-dev/thread-local-allocator","commit_stats":null,"previous_names":["czhao-dev/thread-local-allocator"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/czhao-dev/thread-local-allocator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czhao-dev%2Fthread-local-allocator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czhao-dev%2Fthread-local-allocator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czhao-dev%2Fthread-local-allocator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czhao-dev%2Fthread-local-allocator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/czhao-dev","download_url":"https://codeload.github.com/czhao-dev/thread-local-allocator/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czhao-dev%2Fthread-local-allocator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35344523,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["allocator","benchmarking","c","c11","concurrency","free-list","malloc","memory-allocator","mmap","slab-allocator","systems-programming","tcmalloc","thread-local-storage"],"created_at":"2026-07-10T21:30:34.138Z","updated_at":"2026-07-10T21:30:35.917Z","avatar_url":"https://github.com/czhao-dev.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# memalloc — Thread-Local Memory Allocator in C\n\n[![Language](https://img.shields.io/badge/language-C11-blue.svg)](https://en.cppreference.com/w/c/11)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n\nA from-scratch memory allocator implementing three complementary strategies: a\n**per-thread cache** for lock-free fast-path allocation, a **slab allocator**\nfor fixed-size objects, and a **boundary-tag free list** with immediate\ncoalescing for variable-size allocations. Benchmarked against the system\nallocator across workloads with varying allocation size distributions and\nconcurrency levels.\n\n---\n\n## Overview\n\nMemory allocation is one of the most performance-critical components of a\nsystems runtime. Poor allocator design causes fragmentation that wastes memory,\ncache-inefficient layout that stalls the CPU, and lock contention that throttles\nthroughput in multithreaded programs. Production allocators like jemalloc,\ntcmalloc, and mimalloc each make distinct tradeoffs to address these pressures.\n\n`memalloc` builds the foundational techniques of allocator design from first\nprinciples:\n\n- **Per-thread cache** — eliminates lock acquisition on the common path for\n  small (≤512B) allocations; each thread maintains a free list per size class\n  and only touches a central mutex when refilling or flushing a batch (the\n  tcmalloc design)\n- **Slab allocator** — eliminates internal fragmentation for fixed-size objects\n  by carving a single large allocation into uniform slots, amortizing metadata\n  overhead and preserving object alignment across free/alloc cycles\n- **Free list with boundary tags** — enables O(1) coalescing of adjacent free\n  blocks for variable-size allocations, preventing long-term heap fragmentation\n- **Per-size-class locking** — allows concurrent allocations of different sizes\n  to proceed without contention, scaling better than a single global lock\n\nThe result is a drop-in replacement for `malloc`/`free` (via `LD_PRELOAD` on\nLinux or `DYLD_INSERT_LIBRARIES` on macOS), benchmarked against the system\nallocator on workloads with stable, repeated allocation of same-size objects\n— a pattern common in compilers, game engines, and other latency-sensitive\nnative applications.\n\n---\n\n## Repository Layout\n\n```text\n.\n├── CMakeLists.txt               # top-level: C11, options, MEMALLOC_ENABLE_ASAN\n├── include/memalloc/            # public headers (the allocator API)\n│   ├── allocator.h              # top-level facade (memalloc_allocate/...)\n│   ├── common.h                 # shared constants/helpers\n│   ├── thread_cache.h           # per-thread cache (lock-free fast path)\n│   ├── slab_pool.h              # central slab back-end (one mutex/class)\n│   └── free_list.h              # boundary-tag free list (\u003e512B allocations)\n├── src/                         # implementation\n│   ├── allocator.c              # facade dispatch\n│   ├── thread_cache.c           # ThreadCache impl\n│   ├── slab_pool.c              # SlabPool impl\n│   ├── slab_registry.h / .c     # pointer -\u003e owning slab lookup\n│   ├── free_list.c              # boundary tags + coalescing\n│   ├── mmap_utils.h / .c        # mmap/munmap wrappers\n│   └── malloc_shim.c            # LD_PRELOAD / DYLD_INSERT_LIBRARIES entry points\n├── tests/                       # CTest binaries, one per file\n│   ├── test_alignment.c\n│   ├── test_values.c\n│   ├── test_coalesce.c\n│   ├── test_double_free.c       # forked-child SIGABRT check\n│   ├── test_concurrent.c        # 8 threads x 20,000 ops stress test\n│   └── test_thread_cache.c      # cross-thread free + thread-exit drain\n└── benchmarks/                  # throughput/latency vs. system allocator\n    ├── fixed_size_bench.c\n    ├── mixed_size_bench.c\n    ├── latency_bench.c\n    ├── run_all.sh               # drives all three + the system allocator\n    └── plots/generate_plots.py  # renders README PNGs from results.json\n```\n\n---\n\n## Architecture\n\n```\n                                allocate(size)\n                                       │\n                          ┌────────────────────────┐\n                          │    Allocator Facade    │\n                          └────────────────────────┘\n                                       │\n             size ≤ 512B                            size \u003e 512B\n                   ┌───────────────────┴─────────────────┐\n                   ▼                                     ▼\n       ┌──────────────────────┐            ┌──────────────────────────┐\n       │    ThreadCache       │            │        Free List         │\n       │  (per-thread, lock-  │            │     boundary tags +      │\n       │   free fast path)    │            │        coalescing        │\n       └──────┬───────────────┘            └──────────────────────────┘\n              │ refill / flush batch             mmap-backed arenas\n              ▼ (one lock per batch)\n       ┌──────────────────────┐\n       │      Slab Pools      │\n       │  (central back-end,  │\n       │   one mutex / class) │\n       └──────────────────────┘\n          mmap-backed slabs\n```\n\nThe facade dispatches on request size. Small (≤512B) requests go first to the\ncalling thread's `ThreadCache` — a per-thread free list per size class that\nrequires no locking on the common path. When a thread's bucket empties it pulls\na batch of 32 slots from the central `SlabPool` under one lock; when a bucket\nexceeds 64 slots it flushes 32 back in one lock. Larger requests bypass the\ncache and go directly to the boundary-tag free list. All backing memory comes\nfrom `mmap(2)`, bypassing `brk`/`sbrk` for cleaner address space management.\n\n---\n\n## Slab Allocator Design\n\n### Motivation\n\nConsider a system that repeatedly allocates and frees 64-byte objects. With a\nnaive free list, each allocation carries a header (typically 8–16 bytes),\ninflating the effective object size and introducing internal fragmentation.\nMetadata accumulates throughout the heap, and every allocation requires a search\nof the free list.\n\nA slab allocator solves this by pre-partitioning a large contiguous region (the\n*slab*) into same-size slots, with metadata stored once per slab rather than\nper object:\n\n```\nSlab (mmap'd region, e.g., 4096 bytes for 64-byte slots):\n\n┌──────────────┬───────┬───────┬───────┬───────┬───────────┐\n│  Slab Header │  slot │  slot │  slot │  slot │  ...      │\n│  (freelist,  │  [0]  │  [1]  │  [2]  │  [3]  │           │\n│   count,     │       │       │       │       │           │\n│   next slab) │       │       │       │       │           │\n└──────────────┴───────┴───────┴───────┴───────┴───────────┘\n ▲ one kmalloc/mmap        ▲ 62 usable slots, zero per-slot overhead\n```\n\n### Allocation and Deallocation\n\nThe slab maintains an embedded free list through the slots themselves: each\nfree slot stores a pointer to the next free slot. Allocation is a pointer\npop — O(1), branchless, cache-local. Deallocation is a pointer push — equally\nO(1).\n\n```c\nvoid* memalloc_slabpool_allocate(MemallocSlabPool* pool) {\n    if (!pool-\u003efree_slot) memalloc_slabpool_grow(pool);  // current slab exhausted — map new one\n    void* p = pool-\u003efree_slot;\n    pool-\u003efree_slot = *(void**)pool-\u003efree_slot;  // pop from free list\n    return p;\n}\n\nvoid memalloc_slabpool_deallocate(MemallocSlabPool* pool, void* p) {\n    *(void**)p = pool-\u003efree_slot;  // push onto free list\n    pool-\u003efree_slot = p;\n}\n```\n\n### Cache Alignment\n\nEach slab is `mmap`'d at a page boundary. Slot addresses within the slab are\nnaturally aligned to the slot size (which is always a power of two), satisfying\nthe alignment requirements of any type that fits in that size class. This also\nenables a fast \"which slab owns this pointer\" lookup: mask off the low\n`log2(slab_size)` bits.\n\n---\n\n## Variable-Size Allocator: Boundary Tags and Coalescing\n\n### The Fragmentation Problem\n\nA simple free list without coalescing suffers from *external fragmentation*:\neven when total free memory is sufficient for a request, no single contiguous\nblock may be large enough. A 100-byte request fails if free memory is split into\ntwenty 10-byte fragments.\n\nCoalescing — merging adjacent free blocks into one larger block — is the\nsolution. Efficient coalescing requires O(1) access to a block's neighbors.\n\n### Boundary Tags (Knuth, 1973)\n\nEach block carries a **header** (size + status) at the start and a **footer**\n(same size + status) at the end:\n\n```\n┌────────────────────────────────────────────┐\n│ Header: [size | FREE/ALLOC]  (8 bytes)     │\n├────────────────────────────────────────────┤\n│                                            │\n│  Payload (user-visible memory)             │\n│                                            │\n├────────────────────────────────────────────┤\n│ Footer: [size | FREE/ALLOC]  (8 bytes)     │\n└────────────────────────────────────────────┘\n```\n\nOn `free(p)`:\n1. Mark the block free\n2. Check the **preceding** block's footer — if free, coalesce backward\n3. Check the **following** block's header — if free, coalesce forward\n4. Insert the merged block into the free list\n\nAll three coalesce cases execute in O(1) time — no traversal required.\n\n### Free List Search Strategy\n\nFree blocks are maintained in an explicit doubly-linked free list. `allocate(n)`\nuses **first-fit** search: the first block with `size \u003e= n` is selected and\nsplit if significantly larger than requested. First-fit is O(n) in the number of\nfree blocks but achieves good utilization in practice and is simple to reason\nabout correctly.\n\n**Best-fit** (search entire list for the closest match) reduces fragmentation\nfurther but increases allocation time. **Segregated free lists** (one list per\nsize range) recover O(1) amortized allocation at the cost of implementation\ncomplexity — this is the approach taken by jemalloc.\n\n---\n\n## Thread Safety Model\n\n### Per-thread cache (slab fast path)\n\nFor small allocations (≤512B) the calling thread never acquires a lock on the\ncommon path. Each thread holds a `MemallocThreadCache` (constructed lazily on\nfirst access via `memalloc_thread_cache_for`) with an embedded free list per\nsize class:\n\n```c\nvoid* memalloc_threadcache_allocate(MemallocThreadCache* tc, size_t idx) {\n    MemallocTCBucket* b = \u0026tc-\u003ebuckets[idx];\n    if (b-\u003ecount == 0) refill_from_central_pool(tc, idx);  // one lock, 32 objects\n    void* p = b-\u003ehead;\n    b-\u003ehead = *(void**)p;  // pop — no lock\n    return p;\n}\n```\n\nRefill (bucket empty) and flush (bucket exceeds 64 slots) each acquire the\ncentral `SlabPool`'s per-class mutex exactly once to transfer a batch of 32\nobjects, amortizing contention across N allocations rather than 1.\n\n**Cross-thread free** is handled transparently: an object allocated by thread A\nand freed by thread B is pushed onto B's own bucket for that size class. At\nthe central `SlabPool` level `header_for(p)` identifies the owning slab by\nmasking the pointer, so flushing back works regardless of which thread\noriginally allocated the object.\n\n**Thread-exit cleanup**: since C has no destructors, thread-exit cleanup is\nimplemented with a `pthread_key_t` whose destructor callback flushes all\nbuckets back to the central pools. The `MemallocThreadCache` itself is\ndeliberately backed by an explicit `mmap`'d allocation rather than a plain\n`_Thread_local` object — on platforms where PIC code forces the \"dynamic\" TLS\nmodel (e.g. Apple's dyld TLV implementation), a `_Thread_local` object is\nheap-backed and freed by the runtime's own per-thread cleanup, which can run\nbefore or interleaved with a `pthread_key_t` destructor, making its address\nunsafe to hand to that destructor. A `_Thread_local` *pointer* to the mmap'd\nblock is still used as a fast per-thread cache of \"where is my cache\", since a\nbare pointer has no cleanup ordering hazard of its own. If `malloc` is called\nagain on the same thread after the destructor has run (possible when another\nlibrary's TLS destructor calls malloc during teardown), a `dead` guard flag\nredirects the call directly to the central `SlabPool`, which is safe to call\nfrom any execution context. Residual risk: a `quick_exit` skips\n`pthread_key_t` destructors entirely, leaving cached objects unreachable —\nthis matches the behaviour of tcmalloc and jemalloc, which also accept this\nedge case in exchange for avoiding per-CPU cache complexity.\n\n### Central slab back-end\n\nEach `SlabPool` (one per size class) has its own `pthread_mutex_t`. Threads\nonly contend on a class's mutex when their cache refills or flushes, not on\nevery operation. Concurrent allocations of different size classes are always\ncontention-free.\n\n### Large allocations\n\nThe `FreeListAllocator` (\u003e512B) uses a single mutex over the entire large-object\nheap. Large allocations are less frequent and coalescing requires global\nvisibility of adjacent blocks, so per-thread caching would reintroduce exactly\nthe fragmentation the immediate-coalescing design exists to prevent.\n\n---\n\n## Testing \u0026 Verification\n\nThe test suite ([`tests/`](tests/)) exercises the allocator from the outside —\nthrough the same `allocate`/`deallocate`/`reallocate` facade a real program\nwould use — rather than poking at internal state, so passing tests mean the\npublic contract actually holds.\n\n| Test | What it verifies |\n|------|-------------------|\n| `test_alignment` | Every allocation, across all 7 slab size classes and the free-list path, is aligned to its size class (16 bytes for free-list allocations), and `usable_size()` never reports less than what was requested. |\n| `test_values` | Allocated memory holds exactly the bytes written to it, for both slab and free-list size classes, and `reallocate()` preserves existing contents across a grow. |\n| `test_coalesce` | Freeing adjacent blocks produces correctly merged block sizes via immediate boundary-tag coalescing — including the three-way backward-and-forward merge case — and a subsequent allocation can reuse the fully coalesced region without growing the heap. |\n| `test_double_free` | Freeing the same free-list pointer twice is detected and the process aborts (`SIGABRT`) rather than silently corrupting the heap. Run in a forked child so the crash doesn't take down the test runner. |\n| `test_concurrent` | 8 threads × 20,000 allocate/free operations on random sizes (1–4096 bytes) spanning both the slab pools and the free list, writing and verifying a per-allocation byte pattern to catch any corruption from races. |\n| `test_thread_cache` | Two targeted thread-cache tests: (1) *cross-thread free* — 256 objects allocated in thread A, pattern-verified and freed in thread B; (2) *thread-exit drain* — 64 short-lived threads each allocate and free 80 objects (above the cache high-water mark of 64), then a post-drain batch confirms the memory was returned to the central pool and is reusable with correct contents. |\n\n```\n$ ctest --test-dir build --output-on-failure\n    Start 1: test_alignment\n1/6 Test #1: test_alignment ...................   Passed    0.31 sec\n    Start 2: test_values\n2/6 Test #2: test_values ......................   Passed    0.19 sec\n    Start 3: test_coalesce\n3/6 Test #3: test_coalesce ....................   Passed    0.18 sec\n    Start 4: test_concurrent\n4/6 Test #4: test_concurrent ..................   Passed    0.37 sec\n    Start 5: test_thread_cache\n5/6 Test #5: test_thread_cache ................   Passed    0.22 sec\n    Start 6: test_double_free\n6/6 Test #6: test_double_free .................   Passed    0.20 sec\n\n100% tests passed, 0 tests failed out of 6\n```\n\nAll six also pass cleanly rebuilt with `-DMEMALLOC_ENABLE_ASAN=ON`\n(AddressSanitizer + UndefinedBehaviorSanitizer), including the 160,000-operation\nconcurrent stress test — no use-after-free, heap-buffer-overflow, data race, or\nundefined-behavior reports.\n\n```bash\ncmake -B build-asan -DCMAKE_BUILD_TYPE=Debug -DMEMALLOC_ENABLE_ASAN=ON\ncmake --build build-asan\nctest --test-dir build-asan --output-on-failure\n```\n\nThis sanitizer build earned its keep during development: it surfaced a real\nbug where `reallocate()`'s shrink/grow paths reused the same block-splitting\nroutine as `allocate()`, but without re-checking whether the leftover\nremainder was now adjacent to an already-free block. The result was a quiet\nviolation of the free list's \"no two adjacent free blocks\" invariant — not\nmemory corruption, but unnecessary fragmentation and avoidable arena growth\nunder repeated realloc cycles. A targeted stress test reproduced it directly\n(`free_block_count()` went from 2 to 3 across a single shrinking `reallocate`\nwhere it should have stayed at 2), and the fix — coalescing the remainder\nforward in `split()` before inserting it into the free list — brought it back\nto 2.\n\n---\n\n## Benchmarks\n\nMeasured on Apple Silicon (ARM64) macOS, Apple Clang 21, `-O2`\n(`CMAKE_BUILD_TYPE=Release`). Results compare `memalloc` (loaded via\n`DYLD_INSERT_LIBRARIES` on macOS, or `LD_PRELOAD` on Linux) against the\nplatform's system allocator. Each number below is the **median of 15\nrepeated invocations** of `benchmarks/run_all.sh`, with the observed\nmin–max range shown as an error bar / range — Workloads 1 and 3 complete in\nwell under half a second, so a single run is noisy on a shared, multi-core\ndesktop (background processes competing for the same 8 cores swing\n`fixed_size_bench` by 2–3x run to run). Reproduce with\n`benchmarks/run_all.sh`; regenerate the plots with\n`benchmarks/plots/generate_plots.py` after capturing new runs. Numbers will\nvary by platform, allocator implementation, and machine load.\n\n### Workload 1 — Fixed-size churn (thread-cache advantage)\n\nRepeatedly allocate and free 64-byte objects from 8 concurrent threads.\n\n| Allocator | Throughput (Mops/sec, median) | Min–max (15 runs) | Peak RSS |\n|-----------|-------------------------------|--------------------|----------|\n| system    | 284.6                         | 147.4 – 548.2      | 1.6 MB   |\n| memalloc  | 170.7                         | 135.7 – 368.6      | 1.7 MB   |\n\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"benchmarks/plots/throughput_by_workload_dark.png\"\u003e\n  \u003cimg src=\"benchmarks/plots/throughput_by_workload_light.png\" alt=\"Grouped horizontal bar charts comparing memalloc and system allocator throughput on the fixed-size churn and mixed-size allocation workloads, with min-max error bars.\"\u003e\n\u003c/picture\u003e\n\nAt this wall-clock scale (~0.1–0.4s per run) the two allocators' min–max\nranges overlap heavily, and on this measurement pass the system allocator's\nmedian actually came out ahead — a different result than earlier\nsingle-run measurements of this benchmark suggested. This is a real,\nhonestly-reported result, not a regression in `memalloc` itself: 15 runs\nback-to-back on a loaded development machine is dominated by OS thread\nscheduling variance for a benchmark this short, not by allocator\narchitecture. The thread-cache design still removes all lock acquisition\nfrom the common alloc/free path (the central slab pool mutex is touched\nonly on batch refill/flush, one lock per 32 operations instead of one per\noperation) — that structural advantage is real, but this particular\n8-thread/2M-op workload finishes too quickly on this machine for these 15\nruns to isolate it from scheduling noise. A longer-running or `nice`d,\nquiesced-machine benchmark would be needed to measure the thread-cache\neffect cleanly; treat the numbers above as the honest current measurement,\nnot as a settled verdict either way.\n\n### Workload 2 — Mixed-size allocation (general case)\n\nAllocate objects with sizes drawn from a log-uniform distribution [8, 4096]\nbytes, hold a random subset live, free the rest. Repeat for 10M operations.\n\n| Allocator | Throughput (Mops/sec, median) | Min–max (15 runs) | Fragmentation |\n|-----------|-------------------------------|--------------------|----------------|\n| system    | 30.8                          | 30.4 – 31.0        | 87.5%          |\n| memalloc  | 30.7                          | 30.5 – 30.9        | 87.7%          |\n\nAt 10M operations and ~0.33s of wall time this workload is far more stable\nrun to run (min–max range within ~2% of the median for both allocators) —\nthe two allocators are effectively at parity here. The thread cache applies\nonly to the ≤512B slab path; large allocations still go directly to the\nsingle-mutex free list, so most of the throughput on this size-mixed\ndistribution is set by the shared boundary-tag free list rather than the\ncache. Both allocators report similar fragmentation; this metric is\ndominated by fixed process-baseline RSS relative to the ~0.5 MB of live\nobjects, so it reflects overhead rather than heap layout.\n\n### Workload 3 — Single-threaded latency\n\nMeasure p50 / p99 / p999 allocation latency for 64-byte objects.\n\n| Allocator | p50 (ns) | p99 (ns) | p999 (ns, median) | p999 min–max (15 runs) |\n|-----------|----------|----------|--------------------|--------------------------|\n| system    | 0        | 42       | 834                | 833 – 958                |\n| memalloc  | 0        | 42       | 834                | 792 – 917                |\n\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"benchmarks/plots/latency_percentiles_dark.png\"\u003e\n  \u003cimg src=\"benchmarks/plots/latency_percentiles_light.png\" alt=\"Grouped bar chart on a log scale comparing p50, p99, and p999 single-threaded allocation latency for memalloc and the system allocator.\"\u003e\n\u003c/picture\u003e\n\np50 and p99 are identical between the two allocators — both resolve the\ncommon case in well under the timer's resolution, and single-threaded\nlatency was never where lock contention (the thing the thread cache\ntargets) would show up. p999 medians are also identical at 834ns, with\n`memalloc`'s min–max range shifted slightly lower than the system\nallocator's — a small, consistent-direction effect, but well within the\nnoise band established by Workload 1's variance, so it's reported here\nrather than framed as a proven win.\n\n---\n\n## Design Decisions and Tradeoffs\n\n**Why mmap instead of sbrk for backing memory**\n\n`sbrk` moves the program break to extend the heap as a single contiguous\nregion. `mmap` allocates independent regions at arbitrary addresses. For an\nallocator that manages its own layout, `mmap` is preferable: returned memory\ncan be given back to the OS with `munmap` (shrinking the process footprint),\nwhereas `sbrk`-extended memory can only be returned if it is at the end of the\nheap. `mmap` also avoids the single-threaded contention on the program break.\n\n**Slab threshold of 512 bytes**\n\nThe crossover point between slab and free-list allocation is empirically chosen.\nBelow 512 bytes, the per-object overhead of boundary tags (16 bytes) is a\nsignificant fraction of the allocation, and repeated same-size allocation\npatterns dominate. Above 512 bytes, the diversity of sizes makes size-class\npre-partitioning wasteful, and the free list's flexibility is more valuable.\nThis threshold matches the design of jemalloc (small/large boundary at 512B or\n4KB depending on configuration).\n\n**Immediate vs deferred coalescing**\n\n`memalloc` coalesces immediately on every `free()`. Deferred coalescing (batching\nfrees and coalescing periodically) can improve throughput by amortizing the cost\nacross many frees, but complicates the invariants: a free block's neighbors may\nnot be coalesced yet, requiring additional bookkeeping. Immediate coalescing is\nsimpler to verify correct and produces lower fragmentation at the cost of\nslightly higher per-free overhead.\n\n---\n\n## Building and Running\n\n### Requirements\n\n- C11 compiler (GCC 9+, Clang 10+, Apple Clang)\n- Linux or macOS (uses `mmap`)\n- `cmake` 3.16+\n\n### Build\n\n```bash\ncmake -B build -DCMAKE_BUILD_TYPE=Release\ncmake --build build\n```\n\n### Run benchmarks\n\n```bash\n./build/bin/fixed_size_bench\n./build/bin/mixed_size_bench\n./build/bin/latency_bench\n\n# or compare against the system allocator directly:\n./benchmarks/run_all.sh\n```\n\n### Use as a drop-in replacement\n\n```bash\n# Linux\nLD_PRELOAD=./build/lib/libmemalloc.so ./your_program\n\n# macOS\nDYLD_INSERT_LIBRARIES=./build/lib/libmemalloc.dylib DYLD_FORCE_FLAT_NAMESPACE=1 ./your_program\n```\n\n### Run tests\n\n```bash\nctest --test-dir build --output-on-failure\n```\n\nSee *Testing \u0026 Verification* above for what each test covers and how to run\nthe AddressSanitizer/UndefinedBehaviorSanitizer build.\n\n---\n\n## Future Extensions\n\n- **Per-CPU caches** — replace `thread_local` caches with per-logical-CPU\n  caches (using `sched_getcpu()` / processor affinity), eliminating the\n  TLS-teardown edge case and matching the approach tcmalloc took after its\n  own per-thread phase\n- **Segregated free lists for variable-size** — maintain one free list per\n  power-of-two size range to recover O(1) amortized search (the jemalloc design)\n- **`madvise(MADV_FREE)`** — hint to the kernel that empty slab pages can be\n  reclaimed, reducing RSS under memory pressure without `munmap` overhead\n- **Valgrind / ASan client requests** — annotate allocations so memory\n  debugging tools report errors correctly when `LD_PRELOAD`'d\n- **Idle-cache reclaim** — a background thread that periodically flushes\n  thread caches that have not been active recently, recovering per-thread\n  footprint drift (tcmalloc's \"garbage collection\" mechanism)\n\n---\n\n## References\n\n- Knuth, D.E. *The Art of Computer Programming, Vol. 1* — boundary tag\n  coalescing algorithm (§2.5)\n- Bonwick, J. *The Slab Allocator: An Object-Caching Kernel Memory Allocator*\n  (USENIX 1994) — original slab allocator paper\n- Berger, E. et al. *Hoard: A Scalable Memory Allocator for Multithreaded Applications*\n  (ASPLOS 2000)\n- jemalloc design documentation — https://jemalloc.net/jemalloc.3.html\n- *CS:APP, 3rd Edition*, Bryant \u0026 O'Hallaron — Chapter 9 (dynamic memory\n  allocation), the clearest textbook treatment of boundary tags\n\n---\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fczhao-dev%2Fthread-local-allocator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fczhao-dev%2Fthread-local-allocator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fczhao-dev%2Fthread-local-allocator/lists"}