为什么堆内存这么慢?
堆内存分配涉及系统调用和复杂管理,无序释放导致碎片,增加开销,因此比栈内存慢很多。
为什么堆内存这么慢?
为什么堆内存这么慢?
- 本文基于 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 进行授权