Skip to content
HN On Hacker News ↗

ArenaAllocators don't play nicely with ArrayLists

▲ 36 points 10 comments by ibobev 2w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 394
PEAK AI % 0% · §1
Analyzed
Aug 12
backend: pangram/v3.3
Segments scanned
1 windows
avg 394 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 394 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

A while ago, we looked at ArenaAllocators and why you should view their free as a noop. The short version is that, in order for a free or destroy to actually return memory to the arena, two conditions need to be met: The freed memory must have been the last allocated memory, and The freed memory must have been allocated on the Arena's current node (which is an internal detail of how the Arena grows) Thus, given this code: var a = try arena.alloc(u8, 100); var b = try arena.alloc(u8, 100); arena.free(b); arena.free(a); We can be certain that the memory for b will be returned to the arena, but can't say what will happen to a's memory. Why? b is freed because it was the last allocation and, by definition, had to have come from the arena's current node. After b is freed, a satisfies the first requirement: it becomes the last allocated memory. However, we don't know whether the allocation of b caused a new node to be created; the arena's current node may or may not be the one a came from. If the node thing seems confusing, go read the original post, or just forget about it, because there's a common situation where the first simpler rule isn't met: ArrayList (or Writer.Allocating) growth. When data is appended to an ArrayList it will first try to remap the existing memory (i.e. grow-in-place). But if that fails, it'll allocate a new larger chunk, copy the memory over, and free the previous allocation. See the issue? Here's the code from array_list.zig: const new_memory = try gpa.alignedAlloc(T, alignment, new_capacity); @memcpy(new_memory[0..self.items.len], self.items); gpa.free(old_memory); Even if you aren't interleaving other allocations with your ArrayList growth, the allocate + copy + free guarantees that old_memory isn't the last allocation (the last allocation is new_memory). Is there a solution? Not really, but there are two things you can do. First, size your ArrayList (e.g. with initCapacity or ensureTotalCapacityPrecise). Second, avoid interleaving other allocations with your appends/writes. If you can do that, you'll hit the remap branch much more often and circumvent this issue. Both of those are good to keep in mind regardless of what type of Allocator you're using, but with an ArenaAllocator the worst case is ~3x the memory. I know this is obvious, but I never actually thought about it. I'm probably not the only one.