Devlog #4: Arena Allocator & Test Framework improvements (Commits: bd81ab254f-ba67b8cade)
8 changed files with 1638 additions and 44 deletions
This devlog covers another core module, the Arena Allocator, as well as improvements to the test framework. Implementing this took quite some time, due to the fact that I want to make sure the codebase stays secure and fast.
Arena Allocator (bd81ab254f)
The next core module I implemented is a linear arena allocator, the polar opposite of a general-purpose malloc. The whole purpose is simple: no free individual allocations. Just bump a pointer and reset the whole thing when you’re done. It’s also quite fast.
Alignment without compromise
Every pointer returned by the arena is aligned to max_align_t (typically 16 bytes on x86-64).
When the CPU loads an 8-byte value from an aligned address, the memory controller fetches a single 64-byte cache line in one transaction. The bytes land in the CPU’s load buffer in order, and execution continues the next cycle.
When the same load is unaligned (say, address 0x1003 instead of 0x1000), the 8 bytes straddle two cache lines: 0x1000 and 0x1040. The memory controller now issues two separate cache line fills, the load buffer stitches the result together from the two halves, and the pipeline stalls waiting for the second request to arrive. On modern x86-64 this penalty is roughly 2-3x the latency of an aligned load.
For an allocator focused on speed, every allocation must be aligned. The arena guarantees this in two branchless instructions:
uintptr_t raw = (uintptr_t)arena->memory + arena->offset;
uintptr_t aligned = (raw + 15) & ~15;
Snapshots
The snapshot API (zinn_arena_snapshot_begin / zinn_arena_snapshot_end) lets you checkpoint the arena offset and roll back to it later. In debug builds the rolled-back region is poisoned with 0xAA so dangling reads are immediately obvious.
Unit tests for the Arena (966c4c289e)
47 thorough tests covering just about everything about the arena.
Test Framework improvements
High-precision execution timing (efd7239d47)
Every test function’s runtime is now measured. The runtime is printed next to the PASS/FAIL label with microsecond precision.
Check out the second attachment.
This commit also fixes a subtle C23 compatibility issue: nullptr has type nullptr_t under C23, which can’t be directly cast to uintptr_t. I added a _Generic dispatching macro (ZINN_TO_PTR) that maps nullptr_t to (void*)0 while leaving everything else untouched.
The failure messages also got an upgrade: actual and expected values are now printed numerically instead of a bare expression string.
Test run summary (ba67b8cade)
The original test runner supported one suite at a time. You’d call zinn_test_summary() and get a single block. Now the architecture is multi-suite.
The final output is a detailed breakdown:
- Per-suite: runtime, tests run/passed/failed, assertions made
- Global: success rate percentage, throughput (tests/second), average latency per test, slowest suite with its share of total time
Check out the first attachment if you are curious how it looks.
What’s next?
With memory management settled and testing infrastructure solid, the next milestone is an error handler.