( Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu )
在通常的内存分配领域,常用到一些经典的内存分配库,例如jemalloc(Jason's Malloc)、tcmalloc(Thread Cache Malloc)、dlmalloc(Doug Lea Malloc),Hoard、mimalloc(Micro Malloc)、ptmalloc(POSIX Thread Malloc)等等。
对于其替换原理与替换方式,下面做一些研究与总结,关于实现做一些讨论。
另外关于这些库的选择,文章最后有列出的选项建议。
1. C方法的内存分配函数
对于内存分配来说,对于c方法,我们常用的函数有:malloc、calloc、realloc、free。
这几个函数的函数形态是
- void* malloc(size_t size);
参数size指要分配的内存大小;
成功时返回申请到的内存指针;失败时返回NULL。 - void* calloc(size_t nmemb, size_t size);
参数nmemb是要分配的元素个数,size是元素的大小;相当于要分配(nmemb*size)的内存大小,并把分配出的内存初始化为0;
成功时返回申请到的内存指针,并会把内存初始化为0;失败时返回NULL。 - void* realloc(void* ptr, size_t new_size);
参数ptr是要重新分配内存的指针,new_size是新的内存大小,表示内存块扩容 / 缩容后的新字节数;
成功时返回申请到的内存指针;失败时返回NULL。 - void free(void* ptr);
参数ptr是需要释放内存的指针,指向malloc/calloc/realloc分配出内存的起始位置;
更清晰区分这四个函数:
| 函数 | 函数原型 | 核心参数说明 | 核心行为 | 内存初始化 |
|---|---|---|---|---|
| malloc | void* malloc(size_t size) |
单个参数:总分配字节数 | 基础堆内存分配 | 不初始化(垃圾值) |
| calloc | void* calloc(nmemb, size) |
两个参数:元素个数+单个元素字节数 | 分配内存并整体初始化 | 全初始化为0 |
| realloc | void* realloc(ptr, new_size) |
两个参数:原堆指针+新字节数 | 扩容/缩容已分配的堆内存 | 新增部分不初始化,原数据保留 |
| free | void free(void* ptr) |
单个参数:待释放的堆指针(可NULL) | 释放堆内存,归还给系统 | 无(仅释放) |
2. C++方法的内存分配用法
对于c++方法,我们常用的有:
--- new/new[]
--- delete/delete[]
这两个与malloc/free的不同,主要在于附加了构造函数与析构函数的调用。
- new相当于先malloc出对象的占用内存,然后在该内存上调用构造函数,初始化出对象。
- new T[n]相当于先malloc(sizeof(T)*n)数组对象占用的内存,然后在该内存依次调用数组中对象的构- 造函数,初始化数组对象。
- delete相当于先调用对象的析构函数,然后free内存对象。
- delete[] p相当于先依次调用该内存p数组上所有对象的析构函数,然后free内存对象p。
例如,分配一个对象,释放一个对象:
c
T* p = new T();
delete p;
例如,分配一个对象数组,释放一个对象数组:
c
T* p = new T[n]();
delete[] p;
哪是否能使用C方法来做对象的分配呢?
2.1 如何使用C的方法来做对象分配与删除处理:
这块就要借助c++11之后支持的语法,replacement new,在一个已分配的内存上做函数的构造。
也就是不申请内存,只调用相关的构造函数,从而来支持对象与对象数组的分配。
对于如何调用对象析构,析构时,是通过直接调用析构函数来实现。
例如通过下面方法,方分配一个对象,之后释放一个对象。
c
// 分配
T* p = (T*)malloc(sizeof(T)); //也可采用 ::operator new(_Bytes) 做内存分配,作用同malloc(_Bytes),不带构造
new (p) T();
// 释放
p->~T();
free(p); //也可采用 ::operator delete(p) 做内存释放,作用同free(p),不带析构
例如通过下面方法,分配一个对象数组。
c
// 分配
T* p = (T*)malloc(sizeof(T) * n); //也可采用 ::operator new(_Bytes) 做内存分配,作用同malloc(_Bytes),不带构造
for (int i=0; i<n; i++)
new (&p[i]) T();
// 释放
for (int i=0; i<n; i++)
p[i].~T();
free(p); //也可采用 ::operator delete(p) 做内存释放,作用同free(p),不带析构
2.2 对比:单个对象的分配与释放
| 操作环节 | 通过new与delete自动调用构造析构 | 通过malloc与replacement new手动分配 |
|---|---|---|
| 完整代码 | T* p = new T(); delete p; |
T* p=(T*)malloc(sizeof(T)); new(p)T(); p->~T(); free(p); |
| 内存分配 | 编译器自动 分配sizeof(T)大小内存 |
开发者手动用malloc/内存池分配内存 |
| 构造函数调用 | 编译器自动调用T的无参构造 | 开发者手动调用定位new触发构造 |
| 析构函数调用 | 编译器自动调用T的析构函数 | 开发者手动 调用p->~T()触发析构 |
| 内存释放 | 编译器自动释放内存 | 开发者手动用free/内存池释放内存 |
| 核心优势 | 语法简洁,无手动操作,几乎不会出错 | 解耦内存分配和构造,支持自定义内存管理 |
| 适用场景 | 日常开发、无需自定义内存管理的普通场景 | 内存池、对象池、高性能/特殊内存场景 |
2.3 对比:对象数组的分配与释放
| 操作环节 | 普通版 | 定位new版(手动管理) |
|---|---|---|
| 完整代码 | T* p = new T[n](); delete[] p; |
T* p=(T*)malloc(sizeof(T)*n); for(...)new(&p[i])T(); for(...)p[i].~T(); free(p); |
| 内存分配 | 编译器自动 分配sizeof(T)*n连续内存 |
开发者手动预分配连续大块内存 |
| 构造函数调用 | 编译器自动循环调用n次无参构造 | 开发者手动循环调用定位new触发构造 |
| 析构函数调用 | 编译器自动循环调用n次析构函数 | 开发者手动逆序循环调用析构函数 |
| 内存释放 | 编译器自动释放整体内存 | 开发者手动整体释放预分配内存 |
| 核心优势 | 一行分配、一行释放,无需关注底层细节 | 完全掌控内存和对象生命周期,极致性能优化 |
| 适用场景 | 日常数组开发、普通对象集合 | 高性能内存池、连续内存对象管理、跨模块内存 |
3. 关于使用内存分配库替代
基于上面的分析,我们可以看出,如果需要做内存分配的替代管理,我们可以使用内存分配库接管我们所使用的内存分配方法。
3.1 接管c方法的需要
接管C方法:只要接管malloc, calloc, realloc, free的调用处理姐可以,替代到新的内存分配器对应的函数中。
在代码中替代malloc, free等处理,使用分配CAllocator::Instance().mallloc、CAllocator::Instance().free等在代码中做替代。
也或考虑使用宏方式、预定义变量替换函数(不建议,可能替换不全时,出现申请分配所用的函数不匹配)。
c
// 全局内存分配器:封装tcmalloc,C/C++混合开发通用
class CAllocator {
public:
// 分配bytes字节内存,等价于标准malloc
inline void* malloc(size_t bytes) noexcept {
return tc_malloc(bytes);
}
// 标准calloc接口(元素个数+单个元素字节数),自动初始化内存为0
inline void* calloc(size_t elem_count, size_t elem_bytes) noexcept {
return tc_calloc(elem_count, elem_bytes);
}
// 重新分配内存:ptr为原内存指针,bytes为新的字节数,等价于标准realloc
inline void* realloc(void* ptr, size_t bytes) noexcept {
return tc_realloc(ptr, bytes);
}
// 释放内存:空指针防御,等价于标准free
inline void free(void* ptr) noexcept {
if (ptr != nullptr) {
tc_free(ptr);
}
}
// 【C++扩展】分配并构造C++对象(结合定位new,衔接C++分配器)
// 适用于C++对象的手动内存管理,args为对象构造参数
template <typename T, typename... Args>
inline T* new_obj(Args&&... args) noexcept(false) {
// 先分配内存
void* mem = malloc(sizeof(T));
if (mem == nullptr) {
return nullptr;
}
// 定位new构造对象,完美转发构造参数
try {
return new (mem) T(std::forward<Args>(args)...);
} catch (...) {
// 构造失败,释放已分配的内存
free(mem);
return nullptr;
}
}
// 【C++扩展】析构并释放C++对象
template <typename T>
inline void delete_obj(T* ptr) noexcept {
if (ptr != nullptr) {
// 先手动析构
ptr->~T();
// 再释放内存
free(ptr);
}
}
// 全局单例获取接口:项目唯一实例,全局复用
static CAllocator& Instance() noexcept {
// 局部静态变量,懒加载(第一次调用时初始化),线程安全(C++11及以上)
static CAllocator s_instance;
return s_instance;
}
};
3.2 接管c++方法的需要
接管C++方法:把new与free做重载,重载时使用内存分配库中对应函数做处理,处理时参考手动分配内存的方式来做,调用类的相关构造与析构函数处理。
接管c++方法的需求,需要提供单独的4步处理
一是调用分配内存allocate;
二是调用构造construct;
三是调用析构destroy;
四是调用释放内存deallocate;
下面以stl::allocator替代为例,做内存分配替代的研究,下面的Allocator适合与stl库传入使用。
也适合于独立调用四步处理来使用:allocate、construct、destroy、deallocate。
c
// 工业级极简版:C++11及以上可用,适配所有STL容器
template <typename T>
class MyPoolAllocator {
public:
// 仅必须定义的类型别名
using value_type = T;
// 构造/拷贝/析构:默认即可,无状态分配器
MyPoolAllocator() noexcept = default;
template <typename U>
MyPoolAllocator(const MyPoolAllocator<U>&) noexcept {}
~MyPoolAllocator() noexcept = default;
// 核心:分配内存(对接内存池/tcmalloc/jemalloc)
T* allocate(size_t n) {
if (n == 0) return nullptr;
// 内存不足时,按C++标准抛出std::bad_alloc
if (n > max_size()) throw std::bad_alloc();
void* p = tc_malloc(n * sizeof(T));
if (!p) throw std::bad_alloc();
return static_cast<T*>(p);
}
// 核心:释放内存
void deallocate(T* p, size_t) noexcept {
if (p) tc_free(p);
}
// 可选:construct(可省略,allocator_traits有默认实现)
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
// 定位new + 完美转发,支持任意构造参数
new (static_cast<void*>(p)) U(std::forward<Args>(args)...);
}
// 可选:destroy(可省略,allocator_traits有默认实现)
template <typename U>
void destroy(U* p) {
p->~U();
}
// 可选:max_size(可省略,allocator_traits有默认实现)
size_t max_size() const noexcept {
return static_cast<size_t>(-1) / sizeof(T);
}
};
// 可选-同类型的一致性判断:operator==/!=(可省略,allocator_traits有默认实现)
template <typename T, typename U>
bool operator==(const MyPoolAllocator<T>&, const MyPoolAllocator<U>&) noexcept {
return true;
}
template <typename T, typename U>
bool operator!=(const MyPoolAllocator<T>&, const MyPoolAllocator<U>&) noexcept {
return false;
}
4. 内存分配器选项
4.1 工业界经典选型总结(按场景划分)
| 应用场景 | 首选分配库/方案 | 次选方案 |
|---|---|---|
| 多线程服务器(C/C++) | tcmalloc/jemalloc(无缝替换) | mimalloc/Hoard |
| C++游戏开发 | EASTL/Unreal/Unity内置分配库 | Boost.Pool + tcmalloc |
| C++小对象频繁分配 | Boost.Pool | EASTL/自定义内存池 |
| STL容器性能优化 | EASTL/Boost.Pool | tcmalloc/jemalloc的C++ Allocator |
| 嵌入式/裸机开发 | dlmalloc/lwMalloc/自定义内存池 | uMalloc |
| NUMA架构/超高性能服务器 | Hoard/tcmalloc | jemalloc |
| 轻量小工具/简单程序 | ptmalloc2(系统默认) | dlmalloc |
| 安全敏感场景 | mimalloc(内置安全机制) | tcmalloc + 自定义安全检查 |
4.2 通用使用建议
- 无缝替换优先:如果项目已使用
malloc/free/new/delete,无需修改代码,直接链接tcmalloc/jemalloc即可获得性能提升,是性价比最高的方案; - C++容器必优化:C++标准STL的
std::allocator性能差,建议替换为EASTL (游戏)或Boost.Pool(通用),或基于tcmalloc实现自定义Allocator; - 避免内存碎片的核心:小对象用内存池 (Boost.Pool/EASTL),大对象用tcmalloc/jemalloc ,长期运行的服务优先选jemalloc(碎片控制最优);
- 多线程必选带线程缓存的库:tcmalloc/jemalloc/mimalloc均有线程缓存,避免多线程锁竞争,是高并发的基础;
- 内存跟踪/分析:tcmalloc(pprof)、jemalloc(jeprof/je_stats)、mimalloc(mi_stats/mi_debug)、dlmalloc(自定义统计接口)、EASTL均自带内存分析工具,开发/测试阶段必须开启,排查内存泄漏/碎片问题。