空间配置器:C++ STL 的内存管家
聊到 STL,很多人第一反应是 vector、map 这些容器。但很少有人想过一个问题:容器里的对象,到底是怎么分配内存的?
嗯,这就是空间配置器(allocator)干的事。说白了,它就是 STL 的内存管家。我刚开始用 STL 时,根本没注意过它的存在。直到有一次,我在一个嵌入式项目里频繁创建和销毁小对象,发现性能不对劲……这才回头好好研究了一下这个“幕后英雄”。
空间配置器是什么?
空间配置器,英文叫 allocator。它的职责很简单:为容器分配和释放内存。
每个 STL 容器都有一个默认的 allocator 参数。比如 vector 的声明:
template<typename T, typename Alloc = std::allocator<T>>
class vector;
你平时写 std::vector<int>,其实背后用的是 std::allocator<int>。这个默认配置器,就是 SGI STL 实现的那套经典方案。
我个人习惯把空间配置器理解为“内存批发商”。它从系统拿一大块内存,然后按需分给容器。这样做的好处很明显——减少系统调用,提升性能。
一级空间配置器:直接跟系统打交道
一级空间配置器,说白了就是 malloc/free 的简单封装。
它的逻辑很直接:
- 分配内存时,调用
malloc - 释放内存时,调用
free - 如果
malloc失败,尝试调用用户设置的new_handler,再试一次
代码大概长这样:
class __malloc_alloc_template {
public:
static void* allocate(size_t n) {
void* result = malloc(n);
if (0 == result) {
// 内存不足,尝试调用处理函数
result = oom_malloc(n);
}
return result;
}
static void deallocate(void* p, size_t /*n*/) {
free(p);
}
private:
static void* oom_malloc(size_t n) {
void (*my_malloc_handler)();
while (true) {
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler) {
throw std::bad_alloc();
}
(*my_malloc_handler)();
void* result = malloc(n);
if (result) return result;
}
}
};
你看,一级配置器其实没什么花哨的。它适合分配大块内存。为什么?因为大块内存用 malloc 直接管理,碎片问题不严重。
二级空间配置器:内存池的妙用
这才是 STL 空间配置器的精髓所在。
二级空间配置器专门处理小块内存的分配。它维护了一个内存池,把释放的小块内存缓存起来,下次分配时直接复用。
为什么会这样设计?你想想看,STL 容器里经常存的是 int、char、小结构体。这些对象很小,如果每次都 malloc/free,开销太大了。我在项目中遇到过,一个服务每秒创建和销毁数万个小型对象,用默认配置器性能直接崩了。
二级配置器的核心思路:
- 维护 16 个自由链表(free list),分别管理 8、16、24……128 字节的内存块
- 分配时,从对应链表取一块
- 释放时,把内存块放回链表
- 如果链表为空,从内存池批量申请
示意图如下:
二级配置器的核心代码:
class __default_alloc_template {
private:
// 自由链表,16个
static obj* free_list[16];
// 内存池起始和结束
static char* start_free;
static char* end_free;
static size_t heap_size;
// 根据字节数找到对应的自由链表索引
static size_t FREELIST_INDEX(size_t bytes) {
return (bytes + ALIGN - 1) / ALIGN - 1;
}
// 将字节数对齐到8的倍数
static size_t ROUND_UP(size_t bytes) {
return (bytes + ALIGN - 1) & ~(ALIGN - 1);
}
public:
static void* allocate(size_t n) {
if (n > MAX_BYTES) {
// 大块内存,交给一级配置器
return malloc_alloc::allocate(n);
}
// 小块内存,从自由链表取
obj** my_free_list = free_list + FREELIST_INDEX(n);
obj* result = *my_free_list;
if (result == nullptr) {
// 链表为空,从内存池批量申请
return refill(ROUND_UP(n));
}
*my_free_list = result->next;
return result;
}
static void deallocate(void* p, size_t n) {
if (n > MAX_BYTES) {
// 大块内存,还给系统
malloc_alloc::deallocate(p, n);
return;
}
// 小块内存,插回自由链表
obj** my_free_list = free_list + FREELIST_INDEX(n);
obj* q = (obj*)p;
q->next = *my_free_list;
*my_free_list = q;
}
};
内存池技术:为什么这么快?
内存池的核心思想就一句话:一次申请,多次复用。
传统 malloc 每次分配都要走系统调用,进入内核态,开销很大。而内存池提前从系统申请一大块内存,然后自己管理。
具体来说:
- 批量申请:当自由链表为空时,一次申请 20 块内存(默认值)
- 零碎片:所有小块内存大小都是 8 的倍数,不会产生外部碎片
- 常数时间操作:分配和释放都是 O(1),只是链表指针操作
我曾经在一个高并发服务里做过测试:用默认的 STL 分配器,每秒能处理 5 万次小对象分配;换成自己手写的内存池后,这个数字飙到了 80 万。差距就是这么明显。
避坑指南
我曾经踩过一个坑:在析构函数里释放了内存,但没有通知配置器。结果内存池里的指针全成了野指针,程序跑着跑着就崩了。
记住几个原则:
- 谁分配,谁释放:用配置器分配的内存,一定要用同一个配置器释放
- 不要混用:别在 new/delete 和 allocator 之间混着用,会出大问题
- 线程安全:SGI 的二级配置器不是线程安全的。多线程环境下,要么加锁,要么用线程局部存储
总结
空间配置器是 STL 里最容易被忽略,但又最重要的组件之一。它用一级配置器处理大块内存,用二级配置器配合内存池处理小块内存,既保证了性能,又控制了碎片。
我个人建议,如果你要深入理解 C++ 的性能特性,一定要把空间配置器搞透。它不只是一个分配器,更是一种内存管理思想——把频繁的小操作批量处理,用空间换时间。这个思路在很多场景下都适用。
公众号:蓝海资料掘金营,微信deep3321