What is malloc doing, anyway?
258 points • 3 days ago • Article Link

malloc 函数为内存管理提供了一个看似简单的接口,但它隐藏了程序与操作系统交互时背后的复杂真实情况。开发者只需申请字节并在之后释放,而系统为了避免频繁 syscall 带来的高昂开销,必须完成大量的簿记工作。通过观察一个进程的内存布局可以发现,它被划分为代码段、堆 heap 、栈 stack 等不同区域。当程序需要内存时,通常会移动 program break(也就是堆的末尾边界),而不是为每一次小分配都发起新的 syscall 。

实现一个基础的分配器可以说明简单方案的低效之处。每次请求都移动 program break 的 bump allocator 在功能上是正确的,但由于每次 syscall 都会带来显著的性能惩罚,因此表现不佳。像 glibc 这样的现代分配器会从内核一次性请求大块内存,然后在用户态将其切分使用。为了管理这些切片,系统会在每个内存块上附加 headers,保存必要的 metadata,使 free 函数能够识别块的大小,并维护可用于将来复用的空闲内存列表。

进一步完善这一过程需要实现 splitting 和 coalescing 。当手上有足够大的空闲块时,分配器可以将其 split 以满足小请求;而当内存被释放时,free 函数还能将相邻的空闲块合并,从而减少 fragmentation 。但这又带来了一个难题:如何把内存真正归还给操作系统。由于内核采用 demand paging,只在需要时才把虚拟内存映射到物理 RAM,仅仅在分配器里把内存标记为 free,并不会自动降低进程的 resident set size 。开发者必须手动触发 trim 来让内存释放回内核,不过这通常只有在空闲块位于堆的顶部时才可行。

对于特别大的请求,分配器往往会改用 mmap 系统调用,直接把内存映射到 address space,从而完全绕开堆。这种做法有利之处在于:一旦内存被释放,munmap 可以立即把这部分内存归还给内核,不受它在进程地址空间中位置的影响。虽然这种方式更灵活,但它还需要为新页创建映射,带来额外开销。因此,分配器通常只把这种策略留给大块分配,而把较小、频繁的分配留在堆上完成。

将自定义分配器与生产级的 glibc 实现进行对比,就能看出性能差距从何而来。 glibc 通过使用 bins 、 thread-local caches 等更复杂的结构来获得极快的速度,使系统能迅速找到合适大小的空闲内存,而不必遍历很长的链表。尽管这些优化能显著提升性能,但它们也凸显了内存管理的根本权衡:分配器为了避免与内核交互带来的延迟,会把 chunks 保留在 caches 或 pools 中,从而以"优先保证应用性能"为代价,延后未使用内存的即时释放。

65 comments • Comments Link

• 与传统 `malloc` 抽象相关的复杂性和开销,让人们对"仅栈"或强调生命周期管理的语言产生了兴趣。这类语言试图用更确定的内存模型来取代堆分配。

• 栈式方案并非没有难点:它们通常需要大量预分配、依赖编译器层面的"魔法",而且面对链表等复杂数据结构时,仍可能需要自定义分配器。

• 生产级分配器(例如 `glibc` 中的实现或像 `mimalloc` 这样的专用库)经过高度优化,速度远胜于简单实现。它们通常还能以极低开销应对频繁的小额分配。

• 显式分配器管理是一种宝贵的工程实践。正如在 `Zig` 中看到的那样,强迫开发者明确选择分配策略,比依赖默认的全局堆更能促使人们认真思考并高效处理资源。

• 目标未必是彻底废弃堆内存,而是要打破"共享堆"模型,改用词法作用域内存或由用户显式管理的内存,从而避免许多常见的内存相关错误。

• 通过预先分配大块内存或使用静态缓冲区来规避堆分配,是嵌入式系统和高性能场景中常见的做法;在这些环境里,可预测性往往至关重要。

• 一些语言项目宣称,借助特定的内存模型和稳定的数据布局能够带来性能收益。即便这些说法看起来能回避向量结构中普遍存在的架构权衡,人们通常仍会保持怀疑。

• 阅读分配器的源码,或亲自实现一个简单分配器,能让人更清楚地理解高级应用逻辑与底层系统接口(如 `mmap` 或 `brk`)之间的衔接方式。

• 内核负责最初的虚拟内存供给与物理页映射,但用户空间分配器才承担了绝大多数"魔法":它决定应用程序的内存如何被切分与管理。

• 现代开发在很大程度上已经不再使用 `brk` 和 `sbrk`,而是转向 `mmap` 。原因在于前者已不适用,并且缺少当代内存管理所需的便携性与灵活性。

这场讨论凸显了传统堆分配的便利性,与人们希望获得更确定、更高性能内存控制之间的普遍拉扯。尽管不少人看到了"仅栈"语言以及手动选择分配器的潜力,大家的共识仍很明确:要达到生产可用的内存管理,就不能满足于复杂但抽象度很高、且尚未经过实战检验的方案。深入研究底层实现细节的价值正在变得清晰——这不是为了重复造轮子,而是为了获得编写更稳健、更高效代码所必需的架构清晰度。归根结底,开发者越来越希望减少对不透明的全局堆状态的依赖,转而采用那些能让生命周期更透明、并对内存布局作出更明确定义的方法。