C++程序中的每个动态内存分配都涉及分配器的选择。大多数开发者满足于默认的new和delete,从未意识到分配器对性能和行为的影响。但在高性能计算、游戏开发、嵌入式系统以及实时应用中,分配器的选择可能是决定项目成败的关键因素。理解分配器的内部工作原理,是成为系统级C++开发者的必修课。
参考:https://xbivx.cn/category/travel-advice.html
默认分配器(通常是对malloc/free的封装)是通用解决方案,适用于大多数场景。它管理一个全局堆,处理各种大小的分配请求,通过复杂的数据结构(如空闲链表或伙伴系统)来平衡分配速度、内存碎片和空间利用率。然而,通用性意味着它不是任何场景下的最优解。全局锁的存在使多线程分配成为瓶颈;元数据开销(每个分配块通常需要8-16字节的头部)对小对象不友好;碎片化可能导致内存浪费甚至分配失败。
自定义分配器的动机是多方面的。首先是性能:如果知道分配模式(例如,所有对象大小相同,或者所有分配遵循LIFO顺序),可以设计出比通用分配器快一个数量级的分配器。其次是内存碎片控制:在长时间运行的服务中,碎片可能导致内存耗尽,即使总空闲内存充足。第三是内存池管理:在嵌入式系统中,可能需要从预分配的静态缓冲区中分配内存,避免动态内存分配的不确定性。第四是调试和跟踪:自定义分配器可以记录每次分配的调用栈、大小和时间,帮助检测内存泄漏和性能瓶颈。
竞技场分配器是最简单的自定义分配器之一。它从一大块连续内存开始,维护一个指向当前位置的指针。每次分配时,返回当前指针,然后将指针向前移动请求的大小。释放操作通常不做任何事情(或者整个竞技场一次性释放)。这种分配器极其快速(分配只是指针加法),零碎片(所有分配紧凑排列),但不支持单个释放。竞技场适用于阶段性操作——处理一个请求时分配临时对象,请求处理完毕后一次性释放整个竞技场。
参考:https://xbivx.cn/category/disaster-warning.html
池分配器专为固定大小的对象设计。它维护一个空闲块的单向链表,分配时从链表头部取一个块,释放时将块放回链表。池分配器没有碎片问题(所有块大小相同),分配和释放都是O(1)操作,且内存开销极低(每个空闲块需要一个next指针)。池分配器常用于实现小型对象的快速分配,例如游戏中的粒子系统或网络包缓冲区。
栈分配器类似于竞技场,但支持LIFO顺序的释放。它维护一个栈指针,分配时推进指针,释放时回退指针(但只能按与分配相反的顺序释放)。栈分配器在递归下降解析器或深度优先遍历等场景中非常有用,这些场景天然具有LIFO的内存使用模式。
自由列表分配器是最通用的设计,处理各种大小的分配。它维护一个按地址或大小排序的空闲块链表,分配时查找足够大的块(可能需要分割),释放时将块合并到链表中(与相邻的空闲块合并以减少碎片)。这是malloc/free的典型实现,但可以针对特定场景进行优化——例如,使用大小桶(size buckets)将分配请求分类,加速查找。
STL分配器是C++将自定义分配器集成到标准库的机制。每个容器(如vector、map)都接受一个分配器模板参数,默认为std::allocator。通过提供自定义分配器,可以让容器的所有内存分配都使用特定的分配策略。STL分配器的接口经历了多次修订——C++98的分配器有严重缺陷(如要求相同类型但不同分配器的容器不能交换),C++11引入了“作用域分配器”模型,C++17进一步简化了接口。
参考:https://xbivx.cn/category/weather-knowledge.html
多线程分配器面临额外的挑战。最简单的策略是使用全局锁,但会严重限制并发性。更先进的策略包括:线程本地缓存(每个线程有自己的小块分配器,减少锁竞争)、无锁数据结构(使用原子操作管理空闲列表)、以及分区堆(将堆分割为多个区域,每个线程绑定到特定区域)。现代malloc实现(如tcmalloc、jemalloc)都采用了这些技术。
内存池与对齐是不可忽视的细节。某些硬件(如SIMD指令)要求内存地址对齐到16、32或64字节。默认分配器通常提供最大基本类型对齐(通常是16字节),但可能不满足更高的对齐要求。C++17的new支持align_val_t参数,但自定义分配器需要显式处理对齐。std::aligned_storage和std::aligned_alloc(C11/C++17)是对齐分配的工具。
分配器的“状态”是C++分配器设计中的一个微妙问题。早期STL分配器是无状态的(所有实例行为相同),这允许容器进行某些优化(如空基类优化)。但无状态分配器无法携带分配器的配置信息(如指向特定内存池的指针)。C++11引入了“有状态分配器”,允许分配器实例携带数据,但要求分配器是可复制构造的,并且两个实例可以比较是否相等。这带来了复杂性,但提供了更大的灵活性。
PMR(Polymorphic Memory Resource)是C++17引入的新分配器模型。与传统的基于模板的分配器不同,PMR使用运行时多态:std::pmr::vector使用std::pmr::memory_resource*来执行分配,可以在运行时切换分配策略。PMR提供了一组预定义的资源:new_delete_resource(默认)、synchronized_pool_resource(线程安全池)、unsynchronized_pool_resource(单线程池)、monotonic_buffer_resource(竞技场)。PMR的主要优势是类型擦除——不同的分配器类型不会导致容器类型不同,因此可以在运行时灵活选择分配策略。
自定义分配器不是没有代价的。它们增加了代码复杂度,使内存调试更加困难(因为标准工具可能不识别自定义分配器),并且可能导致难以追踪的bug(例如,从一个分配器分配的内存在另一个分配器中释放)。在大多数应用程序中,默认分配器已经足够好;只有在性能分析明确指出分配器是瓶颈时,才值得引入自定义分配器。
参考:https://xbivx.cn/category/weather-knowledge.html