内存分配器:std::allocator、自定义分配器、STL容器分配器适配、pmr分配器
聊到内存管理,很多人第一反应就是 new 和 delete。但说实话,在真正的工程实践中,尤其是高性能场景下,直接裸用 new 往往不是最优解。我早年做游戏引擎时就被坑过一次——频繁的 new/delete 导致内存碎片化严重,帧率直接掉了一半。从那以后,我开始认真研究分配器。
分配器是什么?说白了,它就是 STL 容器用来申请和释放内存的「幕后管家」。默认情况下,std::vector 用的是 std::allocator,但你可以换掉它,换成自己定制的分配器,甚至用 C++17 引入的 pmr 来统一管理内存池。
std::allocator:默认的分配器
std::allocator 是 STL 容器的默认分配器。它的实现很简单——内部调用 ::operator new 和 ::operator delete。嗯,说白了就是封装了一下全局的 new/delete。
它的接口长这样:
template<typename T>
struct std::allocator {
using value_type = T;
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) noexcept {
::operator delete(p);
}
// 构造和析构(C++20 后已弃用)
template<typename... Args>
void construct(T* p, Args&&... args) {
::new ((void*)p) T(std::forward<Args>(args)...);
}
void destroy(T* p) noexcept {
p->~T();
}
};
注意,allocate 只分配内存,不构造对象。construct 才负责在已分配的内存上调用构造函数。这种分离设计,是 STL 分配器的核心思想——内存分配和对象构造是两回事。
自定义分配器:为什么需要?
默认分配器的问题在于:它每次分配都走全局堆,没有缓存,没有池化。我曾在项目中遇到一个场景——需要频繁创建和销毁大量小对象(比如粒子系统中的粒子)。用默认分配器,性能惨不忍睹。
自定义分配器可以解决这个问题。比如,我们可以实现一个简单的「线性分配器」:
template<typename T>
class LinearAllocator {
char* buffer;
std::size_t offset;
std::size_t capacity;
public:
using value_type = T;
LinearAllocator(std::size_t cap)
: buffer(new char[cap]), offset(0), capacity(cap) {}
~LinearAllocator() { delete[] buffer; }
T* allocate(std::size_t n) {
std::size_t bytes = n * sizeof(T);
if (offset + bytes > capacity)
throw std::bad_alloc();
T* ptr = reinterpret_cast<T*>(buffer + offset);
offset += bytes;
return ptr;
}
void deallocate(T*, std::size_t) noexcept {
// 线性分配器不支持单次释放
}
// 重置整个缓冲区
void reset() { offset = 0; }
};
这个分配器只分配,不释放。用完后一次性重置整个缓冲区。适合帧级内存管理——每帧开始重置,帧内所有临时对象都从这块内存取。我在做实时渲染引擎时就用过这种方案,内存分配几乎零开销。
STL容器分配器适配
STL 容器默认使用 std::allocator,但你可以通过模板参数替换它:
// 使用自定义分配器的 vector
std::vector<int, LinearAllocator<int>> myVec(LinearAllocator<int>(1024));
// 使用 pmr 分配器的 vector
std::vector<int, std::pmr::polymorphic_allocator<int>> pmrVec;
这里有个细节:分配器类型是容器类型的一部分。std::vector<int> 和 std::vector<int, MyAlloc> 是两种不同的类型,不能互相赋值。嗯,这一点在设计接口时要特别注意。
另外,容器在拷贝或移动时,分配器也会被拷贝或移动。C++11 引入了「分配器感知的容器」概念,容器会通过 std::allocator_traits 来统一操作分配器,而不是直接调用分配器的成员函数。这样做的好处是:即使分配器没有提供某些成员(比如 construct),allocator_traits 也会提供默认实现。
pmr分配器:C++17 的现代方案
C++17 引入了 std::pmr(Polymorphic Memory Resources),它提供了一套基于多态的内存分配机制。核心思想是:分配器不再是一个模板参数,而是一个运行时多态对象。
pmr 的核心组件:
- memory_resource:抽象基类,定义
allocate和deallocate接口 - polymorphic_allocator:包装了
memory_resource*的分配器适配器 - synchronized_pool_resource:线程安全的内存池
- unsynchronized_pool_resource:非线程安全的内存池
- monotonic_buffer_resource:线性分配器,类似我们上面写的
LinearAllocator
使用示例:
#include <memory_resource>
#include <vector>
int main() {
// 创建一个 1MB 的缓冲区
char buffer[1024 * 1024];
std::pmr::monotonic_buffer_resource pool(buffer, sizeof(buffer));
// 使用 pmr 分配器的 vector
std::pmr::vector<int> vec(&pool);
for (int i = 0; i < 100000; ++i) {
vec.push_back(i);
}
// 所有内存都来自 buffer,不会走全局堆
}
pmr 的好处是:你可以在运行时切换分配策略。比如,调试时用 new_delete_resource,发布时换成 pool_resource。代码不需要重新编译。
std::pmr::vector 和普通 std::vector,结果在传递数据时发生了意外的拷贝。原因是 pmr 容器的分配器是运行时绑定的,而普通容器的分配器是编译期绑定的。两者不兼容,赋值时会触发深拷贝。所以,要么全用 pmr,要么全不用,别混着来。
知识体系结构图
下面这张图展示了本章涉及的核心概念和它们之间的关系:
如何选择?
我个人的经验是:
- 默认场景:用
std::allocator,别折腾。90% 的情况它够用。 - 性能敏感:考虑
pmr::monotonic_buffer_resource或自定义线性分配器。比如每帧重置的临时数据。 - 多线程环境:用
pmr::synchronized_pool_resource,避免自己实现锁。 - 需要运行时切换策略:pmr 是唯一选择。它的多态特性让分配策略可配置。
最后提醒一句:分配器是工具,不是银弹。滥用自定义分配器反而会让代码变得难以维护。我见过有人为了「优化」给每个容器都配了专属分配器,结果内存碎片更严重了。嗯,适度使用,才是正道。