文章

为什么堆内存这么慢?

堆内存分配涉及系统调用和复杂管理,无序释放导致碎片,增加开销,因此比栈内存慢很多。

为什么堆内存这么慢?

为什么堆内存这么慢?

  • 本文基于 YouTube 频道 Core Dumped 的视频《WHY IS THE HEAP SO SLOW?》讲解整理

程序的内存布局概览

程序运行时,内存大致分为以下几个区域:

地址由高到低
↓

+---------------------------+ ← 高地址
|         栈(Stack)        | ← 用于函数调用、局部变量,从高地址向低地址增长
|      ↓  push/pop          |
+---------------------------+
|         空闲区域           |
+---------------------------+
|         堆(Heap)         | ← 动态分配的内存,从低地址向高地址增长
|      ↑  malloc/new        |
+---------------------------+
|  全局数据段 (.data/.bss)    | ← 存放全局变量与静态变量
+---------------------------+
|       代码段 (.text)       | ← 存放程序指令
+---------------------------+ ← 低地址

为什么需要堆

栈的局限性:

  • 无法存放大量数据
  • 无法存放动态大小的数据结构
  • 函数返回后,局部内存会被释放

堆的优势:

  • 可在运行时从 OS 申请任意大小的内存(malloc/new)
  • 生命周期可控,适合复杂数据结构

为什么堆很慢?

栈的分配非常快,通常只需偏移一个指针; 而堆则慢得多,原因包括:

系统调用开销大

  • 堆分配涉及 brk/mmap 等系统调用
  • 用户态 → 内核态切换 → 上下文切换成本高
+------------+       syscall        +------------+
| 用户程序    | ------------------>  | 操作系统内核 |
+------------+ <------------------  +------------+
         context switch (上下文切换)

复杂的内存管理逻辑

  • 为避免频繁系统调用,库函数如 malloc 会向 OS 一次申请大块内存,再进行细粒度分配
  • 内部分配需维护空闲链表、元数据、对齐等 → 多层逻辑 → 速度慢

堆中的分配与释放

标准 C 接口如下:

1
2
void* malloc(size_t size); // 分配内存
void  free(void* ptr);     // 释放内存

现代分配器如 glibc malloc、jemalloc 通常内部再封装内存池、分配策略等。

内存碎片问题

外部碎片 (External Fragmentation)

已分配:[]
空闲块:____

堆:[]___[]____[]
        ↑
     想申请 6 字节,但碎片不连续 → 失败

内部碎片 (Internal Fragmentation)

申请 14 字节,系统按 16 字节对齐分配 → 浪费 2 字节

堆的分配策略

策略名称特点优缺点
First Fit找第一个够用的空闲块快,但可能留下很多小碎片
Best Fit找最接近所需大小的块最省空闲,遍历慢,易碎片
Worst Fit找最大的空闲块保留大块资源,易浪费小空闲

堆内存释放的无序性与栈的区别

栈的有序释放(LIFO)

栈内存遵循后进先出(LIFO)原则:

  • 新分配的数据必须先释放,旧数据后释放。
  • 这种严格顺序保证栈内存连续紧凑,没有碎片。
  • 分配和释放通过简单的指针移动完成,速度非常快。

堆的无序释放

堆内存分配和释放没有固定顺序:

  • 多线程中,线程 A 和线程 B 分别分配和释放堆内存,释放时间不确定。
  • 线程 A 先释放,线程 B 后释放,堆中留下不连续的空闲区域(“堆洞”)。
  • 这些不连续的空闲空间导致内存碎片,使得连续大块内存变得稀缺。
  • 结果是,堆分配器可能需要调用系统调用获取新的内存,增加开销,降低性能。

假设:

  • 线程 A 分配 50KB 内存,线程 B 分配 80KB 内存。
  • 线程 A 先释放,留下 50KB 空闲区;线程 B 仍占用 80KB。
  • 新请求 70KB 内存时,堆中最大连续空闲区只有 50KB,无法满足需求。
  • 必须触发系统调用请求更多内存,增加分配开销。

堆上的数据结构

链表 (Linked List)

1
2
3
4
struct Node {
    int value;
    Node* next;
};
          +------+     +------+     +------+
栈变量 →  | head | --> |  10  | --> |  20  | --> NULL
          +------+     +------+     +------+

优点:动态增长 缺点:数据不连续,缓存不友好

Vector (STL 描述)

         capacity = 4
         +----+----+----+----+
vector = |  1 |  2 |  3 |  ? |
         +----+----+----+----+
                    ↑
                size = 3

超出时,同步扩容,重新分配+数据拷贝,释放旧地址

堆的成本对比

对比项
分配速度非常快(指针偏移)较慢(需遍历/系统调用)
生命周期自动(函数调用)手动/GC(new/delete)
容量较小较大(可动态调整)
数据结构支持固定/静态数组动态结构,如 vector、list
安全风险溢出内存泄漏、悬置指针、碎片

总结

  • 堆比栈灵活,但速度慢、碎片多、管理复杂
  • 提升堆性能需降低系统调用频率、减少碎片、避免频繁分配
  • 多线程和复杂结构需小心“堆洞”问题
  • STL 结构、垃圾回收、定制分配器都是对堆性能的进一步优化
本文由作者按照 CC BY 4.0 进行授权