ArenaAllocators 与 ArrayLists 的内存陷阱

ArenaAllocators don't play nicely with ArrayLists

在使用 ArenaAllocators 时,很多人以为 free 操作能轻松回收内存,但实际情况要复杂得多。只有当释放的内存是最后分配的,且来自 Arena 的当前节点时,内存才会真正归还。而 ArrayList 在扩容时,会先分配新内存、复制数据,再释放旧内存,这导致旧内存不再是最后分配的,从而无法被 Arena 回收。即使不穿插其他分配操作,ArrayList 的扩容机制也会让 ArenaAllocators 的内存回收失效。虽然无法彻底解决,但可以通过预设容量或避免混合分配来缓解问题。否则,最坏情况下内存占用可能达到预期的三倍。

即使你不将其他分配操作与 ArrayList 的扩容穿插在一起,分配加复制再加释放的机制也保证了旧内存不再是最后分配的内存。
  1. tynorf

    FWIW,如果你没有交错进行其他分配(包括在其他线程上),Zig 中的 ArenaAllocator 就没有这个问题。如果你尝试调整最近一次分配的内存大小,它会在可能的情况下就地完成。

  2. nostrademons

    这篇文章(以及上一篇)有点奇怪,因为这两篇里都没有讨论为什么要使用 arena,我不确定作者是否理解这个非常基础的概念。

    当你有一段计算可能会消耗大量临时内存,并且希望在计算完成后一次性释放所有这些内存时,就会使用 arena。它本质上是一种动态作用域的内存管理,非常类似于在栈上分配一个大结构体然后让栈指针自动释放它,但不需要你在编译时就知道要分配的所有内存的大小,也不需要分配遵循严格的 LIFO 顺序。它们之所以非常快,原因与栈分配和复制式 GC 一样:只是移动一个分配指针。但与复制式 GC 不同的是,释放内存也很快,因为你只需一次性释放整个 arena。

    free() 在 arena 中成为空操作是顺理成章的,这基本上就是它的核心目的。

    但 ArrayList 同样无法与之良好协作,这也是顺理成章的。ArrayList(或 Vector)有自己的动态内存管理;当空间不足时,它会透明地进行重新分配。这很方便,但如果你追求性能——而这基本上是使用 Arena 的主要原因——你就需要对复制和分配更加小心。

    通常,基于 arena 的分配的常见模式是:先进行一轮规划并计算数据应存放的位置,然后……

  3. wasmperson

    我所有的“arena”都有一个额外的固定长度函数指针列表,在重置/释放内存之前会按顺序调用它们。这样它们就能管理任何形式的内存(或非内存资源):

    char *dat = malloc(42);

    arena_push_dtor(ar, dat, free);

    // 使用 dat

    巧妙地解决了那些不适合放入线性内存的问题,同时还能让你懒洋洋地处理清理工作。

    另外:如果你不需要连续性,你可以像 arena 内部处理自身内存那样,将动态数组拆分成链接的桶。迭代和随机访问仍然会很快。

同日更多故事

2026-08-12