自由链表(FreeList)
这是内存池的基础数据结构,将空闲内存块串成链表:
class FreeList { private: void* head_; // 链表头指针 size_t size_; // 当前大小 size_t max_size_; // 慢启动最大批量大小 public: void Push(void* obj); void* Pop(); void PushRange(void* start, void* end, size_t n); size_t PopRange(void*& start, void*& end, size_t n); };巧妙设计:利用空闲内存块本身存储链表指针,零额外开销!
static inline void*& NextObj(void* obj) { return *(void**)obj; // 前8字节存储下一个块的地址 }2. Span结构
Span是管理连续页面的核心结构:
struct Span { PageID page_id_; // 起始页号 size_t n_; // 页数 Span* next_; // 双向链表指针 Span* prev_; size_t object_size_; // 切分的对象大小 size_t use_count_; // 已分配对象数 void* free_list_; // 切分后的自由链表 bool is_used_; // 是否使用中 };关键算法实现
1. 大小类别映射算法
将任意大小映射到固定的大小类别,这是性能的关键:
static inline size_t RoundUp(size_t size) { if (size <= 128) { return Align(size, 8); // 8字节对齐 } else if (size <= 1024) { return Align(size, 16); // 16字节对齐 } else if (size <= 8 * 1024) { return Align(size, 128); // 128字节对齐 } else if (size <= 64 * 1024) { return Align(size, 1024); // 1KB对齐 } else if (size <= 256 * 1024) { return Align(size, 8 * 1024); // 8KB对齐 } }设计考量:小对象精细对齐,大对象粗粒度对齐,平衡内存利用率和性能。
2. 慢启动批量分配
动态调整批量大小,平衡内存使用和性能:
static size_t NumMoveSize(size_t size) { size_t base_batch; if (size <= 32) { base_batch = 128; // 小对象大批量 } else if (size <= 128) { base_batch = 64; } else if (size <= 512) { base_batch = 32; } else { base_batch = 16; // 大对象小批量 } return base_batch * batch_multiplier; }3. 页面映射优化
采用二层基数树,快速查找对象所属的Span:
template<int BITS> class PageMap2 { private: static const int LEAF_BITS = BITS / 2; static const int ROOT_BITS = BITS - LEAF_BITS; struct OptimizedLeaf { SubLeaf* sub_leafs[SUB_LEAFS_PER_LEAF]; // 延迟初始化,减少内存开销 }; public: inline void* get(size_t k) const; inline void set(size_t k, void* v); };上面展示的是部分核心设计思路的简化代码,实际实现中还包含了更多的边界处理和优化细节。
PS:说实话,能参考TCMalloc架构手搓高性能内存池的人应该不多。我在研究阶段看了网上的几个版本,发现大部分还是基于32位系统设计的,在如今的64位环境下就显得有些局限了。可能是早期教学项目的代码被反复借鉴,缺少针对现代系统的深度优化。
注意: 我这个版本从头开始针对64位系统设计,不仅支持完整的虚拟地址空间,还考虑了现代CPU架构的特性, 至少在设计思路上更贴近实际应用场景。
手把手教你实现C++高性能内存池,相比 malloc 性能提升7倍!
性能优化技巧
1. 分支预测优化
// 利用__builtin_expect优化分支预测 if (__builtin_expect(!list.Empty(), 1)) { return list.Pop(); // 大概率走这个分支 }2. 内联函数优化
// 热点函数全部内联 static inline size_t GetPageID(void* addr) { return reinterpret_cast<PageID>(addr) >> PAGE_SHIFT; }3. 缓存友好的数据结构
// 64字节对齐,匹配CPU缓存行 struct SimpleBatch { void* ptrs[32]; uint8_t count = 0; } __attribute__((aligned(64)));4. 锁优化策略
// 桶锁:每个大小类别独立锁 std::mutex mutexes_[NFREELISTS]; // 减少持锁时间 { std::lock_guard<std::mutex> lock(mutexes_[index]); // 最少的临界区代码 }5. 基于perf的性能分析优化
在内存池开发过程中,perf是我最重要的性能分析工具。下面分享三个实际优化案例:
案例1:发现热点函数并优化
问题发现:使用perf分析发现SizeClass::Index()函数占用了15%的CPU时间
# 性能分析命令 sudo perf record -g ./test_memory_pool sudo perf report # 发现热点 15.23% test_memory_pool [.] SizeClass::Index(unsigned long) 8.94% test_memory_pool [.] ThreadCache::Allocate(unsigned long)优化方案:针对最常用的小对象做特殊优化
// 优化前:每次都走复杂的Index计算 size_t index = SizeClass::Index(align_size); // 优化后:小对象直接计算,避免函数调用 size_t index; if (__builtin_expect(align_size <= 128, 1)) { index = (align_size >> 3) - 1; // 直接位运算 } else { index = SizeClass::Index(align_size); // 复杂情况才调用 }效果验证:再次perf分析,该函数CPU占用降到3.2%,整体性能提升12%
案例2:优化Deallocate的批量处理
问题发现:Deallocate函数中频繁的Push操作CPU耗时较高
12.45% test_memory_pool [.] FreeList::Push(void*) 7.33% test_memory_pool [.] ThreadCache::Deallocate(void*, unsigned long)优化方案:针对小对象使用批量释放策略
// 优化前:每次都要操作链表 void ThreadCache::Deallocate(void* ptr, size_t size) { size_t index = GetIndex(size); free_lists_[index].Push(ptr); // 每次都要修改链表头 } // 优化后:使用批量缓冲区 SimpleBatch batches_[32]; // 只为热点大小创建批量 void ThreadCache::Deallocate(void* ptr, size_t size) { if (index < 32) { SimpleBatch& batch = batches_[index]; batch.ptrs[batch.count++] = ptr; if (batch.count >= 32) { FlushSimpleBatch(index, size); // 批量刷新到链表 } } }