竞技场分配器(Arena Allocators)与动态数组(ArrayLists)并不兼容。
ArenaAllocators don't play nicely with ArrayLists

原始链接: https://www.openmymind.net/Arena-Allocators-and-ArrayLists/

这篇文章解释了为什么 `ArenaAllocator.free()` 在大多数实际场景中基本等同于“空操作”(no-op)。 若要通过 Arena 回收内存,被释放的块必须是最近一次分配的内存,且位于 Arena 当前的内部节点上。由于这些内部条件很难同时满足,调用 `free` 通常无法真正归还内存。 这一问题在 `ArrayList` 等动态结构中表现最为明显。当 `ArrayList` 扩容时,它会分配新内存、拷贝现有数据并释放旧块。由于新块变成了最近一次的分配,旧块无法被 Arena 回收。这一过程会导致严重的内存膨胀,其占用空间可能达到实际需求的三倍。 为缓解这一问题,开发者应: 1. **预设集合容量:** 使用 `initCapacity` 或 `ensureTotalCapacityPrecise` 来避免不必要的重新分配。 2. **减少交替分配:** 在扩容集合时,尽量避免进行其他内存分配,以提高“原地扩容”操作发生的概率。 尽管这些做法对任何分配器都有益,但在使用 `ArenaAllocator` 时,它们对于防止内存失控增长至关重要。

``` Hacker News 最新 | 过往 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Arena 分配器与 ArrayList 配合并不理想 (openmymind.net) 6 分,ibobev 发布于 2 小时前 | 隐藏 | 过往 | 收藏 | 1 条评论 tynorf 56 分钟前 [–] 仅供参考,如果你没有进行其他分配(包括在其他线程上的分配),Zig 中的 ArenaAllocator 就不会有这个问题。如果你尝试调整最近一次分配的大小,它会尽可能地在原地进行。https://ziglang.org/documentation/0.16.0/std/#std.heap.Arena... 回复 准则 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索: ```
相关文章

原文

Aug 04, 2026

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:

  1. The freed memory must have been the last allocated memory, and
  2. 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.

联系我们 contact @ memedata.com