关于内存分配的优化与设计

( 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 通用使用建议
  1. 无缝替换优先:如果项目已使用malloc/free/new/delete,无需修改代码,直接链接tcmalloc/jemalloc即可获得性能提升,是性价比最高的方案;
  2. C++容器必优化:C++标准STL的std::allocator性能差,建议替换为EASTL (游戏)或Boost.Pool(通用),或基于tcmalloc实现自定义Allocator;
  3. 避免内存碎片的核心:小对象用内存池 (Boost.Pool/EASTL),大对象用tcmalloc/jemalloc ,长期运行的服务优先选jemalloc(碎片控制最优);
  4. 多线程必选带线程缓存的库:tcmalloc/jemalloc/mimalloc均有线程缓存,避免多线程锁竞争,是高并发的基础;
  5. 内存跟踪/分析:tcmalloc(pprof)、jemalloc(jeprof/je_stats)、mimalloc(mi_stats/mi_debug)、dlmalloc(自定义统计接口)、EASTL均自带内存分析工具,开发/测试阶段必须开启,排查内存泄漏/碎片问题。

( Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu )

相关推荐
范纹杉想快点毕业1 小时前
状态机设计与嵌入式系统开发完整指南从面向过程到面向对象,从理论到实践的全面解析
linux·服务器·数据库·c++·算法·mongodb·mfc
坚定学代码2 小时前
认识 ‘using namespace‘
c++
jiang_changsheng2 小时前
环境管理工具全景图与深度对比
java·c语言·开发语言·c++·python·r语言
LYOBOYI1232 小时前
qml的对象树机制
c++·qt
LeoZY_2 小时前
开源项目精选:Dear ImGui —— 轻量高效的 C++ 即时模式 GUI 框架
开发语言·c++·ui·开源·开源软件
特立独行的猫a3 小时前
C++轻量级Web框架介绍与对比:Crow与httplib
开发语言·前端·c++·crow·httplib
YXXY3133 小时前
模拟实现map和set
c++
阿猿收手吧!3 小时前
【C++】引用类型全解析:左值、右值与万能引用
开发语言·c++
「QT(C++)开发工程师」3 小时前
C++ 策略模式
开发语言·c++·策略模式