highpool 高并发内存池性能测试报告
1. 测试背景
highpool 是一个基于 TCMalloc 思路实现的 C++ 高并发内存分配器。当前实现采用三级缓存结构:
ThreadCache:线程本地缓存,每个线程独立维护自由链表,常规小对象分配路径无锁。CentralCache:中心缓存,在线程本地缓存不足或本地缓存过多时,负责批量分配与批量回收。PageCache:页级缓存,负责向系统申请大块内存、拆分Span、回收空闲Span并合并相邻页。
本次测试的目标不是只证明某一个固定场景下的加速比,而是观察 highpool 在不同线程数、不同申请大小下的性能边界,并与系统 malloc/free 做对比。
2. 测试环境
| 项目 | 配置 |
|---|---|
| 操作系统 | Windows |
| CPU | 8 核 16 线程 |
| 编译器 | Visual Studio 2022 MSVC v143 |
| 构建工具 | MSBuild |
| 构建配置 | Release / x64 |
| 对比对象 | C 标准库 malloc/free |
| 测试入口 | highpool/Benchmarkcpp.cpp |
说明:Debug 模式下编译器优化不足,且存在额外调试开销,不能代表真实运行性能。因此本文以 Release / x64 的测试结果作为主要分析依据。
3. 测试方法
测试流程:
- 创建指定数量的工作线程。
- 所有线程通过条件变量同步起跑。
- 每个线程循环执行多轮"批量申请 -> 批量释放"。
- 分别统计 highpool 和
malloc/free的耗时。 - 使用
malloc/free耗时除以 highpool 耗时,得到加速比。
加速比计算方式:
text
speedup = malloc_free_time / highpool_time
含义:
text
speedup > 1:highpool 更快
speedup = 1:两者接近
speedup < 1:malloc/free 更快
总操作数计算方式:
text
ops = threads * rounds * n
为了避免大块内存测试时峰值内存过高,不同申请大小使用了不同的 n。小对象使用更高循环次数,大对象降低每轮申请数量。
| 申请大小 | 每轮每线程申请次数 n | rounds |
|---|---|---|
| 16B | 100000 | 20 |
| 64B | 100000 | 20 |
| 256B | 50000 | 20 |
| 1024B | 20000 | 20 |
| 4096B | 5000 | 20 |
| 16384B | 1000 | 20 |
| 65536B | 256 | 20 |
| 200000B | 64 | 20 |
测试线程数:
text
1, 2, 4, 8, 16
4. 已知大小释放接口优化
原始释放接口为:
cpp
ConcurrentFree(void* ptr)
该接口释放时需要通过对象地址反查所属 Span,再得到对象大小:
cpp
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
size_t size = span->_objSize;
在小对象高频释放场景下,这个映射查询会被大量放大。
本次新增已知大小释放接口:
cpp
ConcurrentFree(void* ptr, size_t size)
benchmark 中本来就知道申请大小,因此 highpool 测试路径改为:
cpp
[allocSize](void* ptr) { ConcurrentFree(ptr, allocSize); }
这样可以在小对象释放路径中跳过 MapObjectToSpan 查询。旧接口仍然保留,不影响原有调用方式。
5. Speedup 总览矩阵
下表展示不同申请大小、不同线程数下的加速比。
| size | 1 线程 | 2 线程 | 4 线程 | 8 线程 | 16 线程 |
|---|---|---|---|---|---|
| 16B | 4.17x | 2.72x | 0.93x | 0.45x | 0.21x |
| 64B | 5.84x | 4.44x | 1.74x | 0.66x | 0.28x |
| 256B | 3.00x | 2.12x | 1.12x | 0.56x | 0.32x |
| 1024B | 2.29x | 1.16x | 0.38x | 0.21x | 0.21x |
| 4096B | 1.72x | 0.73x | 0.31x | 0.19x | 0.26x |
| 16384B | 3.90x | 0.86x | 0.71x | 0.28x | 0.10x |
| 65536B | 4.18x | 6.49x | 16.29x | 14.54x | 3.44x |
| 200000B | 7.08x | 15.49x | 10.83x | 14.06x | 3.26x |
从总览结果可以看到:
- 16B、64B、256B 小对象在 1 到 4 线程时表现较好,但到 8、16 线程后性能明显下降。
- 1024B 到 16384B 区间在多线程下整体弱于
malloc/free。 - 65536B 和 200000B 在本轮测试中 highpool 优势明显,但这类结果受测试模型影响较大,因为 highpool 会复用页缓存,而
malloc/free需要面对更通用的堆管理路径。 - 当前实现最明显的问题不是单线程性能,而是多线程同尺寸热点场景下的共享锁竞争。
6. 完整测试数据
6.1 16B
| threads | ops | highpool | malloc/free | speedup |
|---|---|---|---|---|
| 1 | 2,000,000 | 24.001 ms | 100.056 ms | 4.17x |
| 2 | 4,000,000 | 31.993 ms | 87.017 ms | 2.72x |
| 4 | 8,000,000 | 97.664 ms | 90.671 ms | 0.93x |
| 8 | 16,000,000 | 337.037 ms | 150.142 ms | 0.45x |
| 16 | 32,000,000 | 1122.106 ms | 230.042 ms | 0.21x |
6.2 64B
| threads | ops | highpool | malloc/free | speedup |
|---|---|---|---|---|
| 1 | 2,000,000 | 32.325 ms | 188.626 ms | 5.84x |
| 2 | 4,000,000 | 37.232 ms | 165.469 ms | 4.44x |
| 4 | 8,000,000 | 125.403 ms | 218.446 ms | 1.74x |
| 8 | 16,000,000 | 420.975 ms | 278.108 ms | 0.66x |
| 16 | 32,000,000 | 1421.001 ms | 395.529 ms | 0.28x |
6.3 256B
| threads | ops | highpool | malloc/free | speedup |
|---|---|---|---|---|
| 1 | 1,000,000 | 21.722 ms | 65.129 ms | 3.00x |
| 2 | 2,000,000 | 26.193 ms | 55.493 ms | 2.12x |
| 4 | 4,000,000 | 70.299 ms | 78.987 ms | 1.12x |
| 8 | 8,000,000 | 258.965 ms | 146.194 ms | 0.56x |
| 16 | 16,000,000 | 787.070 ms | 253.987 ms | 0.32x |
6.4 1024B
| threads | ops | highpool | malloc/free | speedup |
|---|---|---|---|---|
| 1 | 400,000 | 12.164 ms | 27.916 ms | 2.29x |
| 2 | 800,000 | 20.712 ms | 23.996 ms | 1.16x |
| 4 | 1,600,000 | 67.493 ms | 25.850 ms | 0.38x |
| 8 | 3,200,000 | 199.391 ms | 42.082 ms | 0.21x |
| 16 | 6,400,000 | 560.470 ms | 119.871 ms | 0.21x |
6.5 4096B
| threads | ops | highpool | malloc/free | speedup |
|---|---|---|---|---|
| 1 | 100,000 | 6.072 ms | 10.440 ms | 1.72x |
| 2 | 200,000 | 9.476 ms | 6.886 ms | 0.73x |
| 4 | 400,000 | 29.361 ms | 9.086 ms | 0.31x |
| 8 | 800,000 | 114.752 ms | 22.369 ms | 0.19x |
| 16 | 1,600,000 | 321.072 ms | 83.797 ms | 0.26x |
6.6 16384B
| threads | ops | highpool | malloc/free | speedup |
|---|---|---|---|---|
| 1 | 20,000 | 1.668 ms | 6.503 ms | 3.90x |
| 2 | 40,000 | 3.196 ms | 2.752 ms | 0.86x |
| 4 | 80,000 | 7.071 ms | 4.990 ms | 0.71x |
| 8 | 160,000 | 26.335 ms | 7.339 ms | 0.28x |
| 16 | 320,000 | 154.327 ms | 14.973 ms | 0.10x |
6.7 65536B
| threads | ops | highpool | malloc/free | speedup |
|---|---|---|---|---|
| 1 | 5,120 | 0.735 ms | 3.075 ms | 4.18x |
| 2 | 10,240 | 1.878 ms | 12.194 ms | 6.49x |
| 4 | 20,480 | 4.205 ms | 68.487 ms | 16.29x |
| 8 | 40,960 | 13.474 ms | 195.948 ms | 14.54x |
| 16 | 81,920 | 122.568 ms | 421.357 ms | 3.44x |
6.8 200000B
| threads | ops | highpool | malloc/free | speedup |
|---|---|---|---|---|
| 1 | 1,280 | 0.363 ms | 2.571 ms | 7.08x |
| 2 | 2,560 | 0.661 ms | 10.242 ms | 15.49x |
| 4 | 5,120 | 2.157 ms | 23.353 ms | 10.83x |
| 8 | 10,240 | 3.750 ms | 52.708 ms | 14.06x |
| 16 | 20,480 | 33.665 ms | 109.714 ms | 3.26x |
7. 结果分析
7.1 小对象场景:线程数越多,优势越弱
以 64B 为例:
| threads | speedup |
|---|---|
| 1 | 5.84x |
| 2 | 4.44x |
| 4 | 1.74x |
| 8 | 0.66x |
| 16 | 0.28x |
这说明 highpool 的单线程和少线程小对象分配路径是有效的,ThreadCache 的无锁自由链表能显著降低分配成本。
但线程数继续增加后,优势快速下降。原因是所有线程都在申请同一种大小时,会集中竞争同一个 CentralCache 桶锁:
cpp
_spanLists[index]._mtx
当 ThreadCache 本地缓存不够,或者本地自由链表过长需要归还对象时,都会进入 CentralCache。线程越多,这条共享路径越容易成为瓶颈。
7.2 16 线程下,64B 场景仍然没有超过 malloc/free
16 线程、64B 场景中:
text
highpool: 1421.001 ms
malloc/free: 395.529 ms
speedup: 0.28x
虽然已经新增 ConcurrentFree(ptr, size),释放时可以跳过对象到 Span 的映射查询,但结果仍然不理想。这说明在该场景中,主要瓶颈已经不是单次释放查询,而是:
CentralCache热点桶锁竞争。PageCache全局锁竞争。ThreadCache与CentralCache交互过于频繁。
7.3 中等大小对象在多线程下表现较弱
1024B、4096B、16384B 在多线程下普遍弱于 malloc/free。
例如 4096B:
| threads | speedup |
|---|---|
| 1 | 1.72x |
| 2 | 0.73x |
| 4 | 0.31x |
| 8 | 0.19x |
| 16 | 0.26x |
这类对象的特点是:
- 单个对象已经不算很小。
- 每个 span 能切出来的对象数量减少。
- 批量搬运数量下降。
- 更容易触发
CentralCache和PageCache交互。
因此,highpool 的本地缓存优势被削弱,而共享缓存层的锁成本被放大。
7.4 大块对象测试中 highpool 表现较好,但需要谨慎解释
65536B 和 200000B 场景中 highpool 表现明显优于 malloc/free。
例如 65536B:
| threads | speedup |
|---|---|
| 1 | 4.18x |
| 2 | 6.49x |
| 4 | 16.29x |
| 8 | 14.54x |
| 16 | 3.44x |
这说明 highpool 的页级缓存复用在大块内存场景下有明显优势。但是这类结果需要谨慎解释:
- 测试模型是固定大小、重复申请释放。
- highpool 可以复用之前申请到的 span。
malloc/free是通用堆实现,需要兼顾更复杂的场景。- 大块测试的 ops 较少,部分结果更容易受系统状态影响。
因此,大块结果可以说明 highpool 的 page/span 复用机制有效,但不能简单等价为所有大对象场景都一定优于系统堆。
8. 瓶颈定位
8.1 CentralCache 热点桶锁
当前 CentralCache 按 size class 维护 span 链表。虽然不同 size class 之间有不同的锁,但同一个 size class 只有一个桶锁。
当 16 个线程同时测试 64B 时,所有线程都集中在同一个桶:
cpp
_spanLists[index]._mtx
这会导致明显锁竞争。
8.2 PageCache 全局锁
当 CentralCache 无法满足请求时,需要进入 PageCache:
cpp
PageCache::GetInstance()->_pageMtx
该锁是全局锁。线程数越多,进入 page cache 时阻塞越明显。
8.3 ThreadCache 回收策略偏保守
当前线程本地自由链表达到 MaxSize 后,会归还一批对象给 CentralCache:
cpp
if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
{
ListTooLong(_freeLists[index], size);
}
如果归还过于频繁,就会增加 ThreadCache -> CentralCache 的交互次数,从而放大锁竞争。
8.4 系统 malloc/free 本身已经高度优化
Windows 下的 malloc/free 并不是简单的单全局锁堆。现代运行库和系统堆已经针对多线程、小对象、缓存局部性做过大量优化。
因此,一个教学版内存池在少线程场景超过 malloc/free 是合理的,但在 16 线程热点场景被反超也很正常。
9. 优化建议
9.1 提高热点小对象的 ThreadCache 缓存上限
对 16B、64B、256B 这类高频小对象,可以允许每个线程缓存更多对象,减少回 central cache 的次数。
预期收益:
- 提高 ThreadCache 命中率。
- 减少
CentralCache桶锁竞争。 - 改善 8 线程、16 线程小对象场景。
代价:
- 每个线程可能持有更多空闲内存。
- 峰值内存占用会上升。
9.2 调整 batch 策略
当前小对象批量移动上限为:
cpp
if (num > 512)
num = 512;
可以按大小分段调整:
text
<= 64B: batch 上限提高到 1024 或 2048
<= 256B: 维持 512 或小幅提高
更大对象: 保持较小 batch,避免内存浪费
9.3 对 CentralCache 做分片
当前同一个 size class 只有一个中心链表。可以考虑把热点 size class 拆成多个 shard:
text
CentralCache[ size_class ][ shard_id ]
shard_id 可以根据线程 id、CPU id 或哈希值选择。
这样 16 个线程不会全部竞争同一把锁。
9.4 优化 benchmark
当前测试已经比单点测试更完整,但仍可以继续完善:
- 每组测试重复多次,取中位数。
- 增加随机大小混合测试。
- 增加跨线程释放测试。
- 增加峰值内存统计。
- 增加 CPU 利用率和锁等待统计。
10. 结论
当前 highpool 的性能特征比较清晰:
text
少线程 + 小对象:
highpool 优势明显,ThreadCache 无锁快路径有效。
多线程 + 同尺寸热点小对象:
highpool 优势下降,CentralCache/PageCache 锁竞争成为主要瓶颈。
中等大小对象 + 多线程:
当前实现普遍弱于 malloc/free,需要优化 batch 和中心缓存交互。
大块对象 + 重复申请释放:
highpool 的 span/page 复用机制表现较好,但结果依赖测试模型。
本轮测试说明,highpool 已经具备自定义内存池的核心机制,并能在部分场景下明显超过 malloc/free。但在 8 到 16 线程的高并发热点场景中,当前实现还没有充分发挥多线程扩展性。
后续优化重点应放在:
- 减少
ThreadCache与CentralCache的交互频率。 - 提高热点小对象的本地缓存能力。
- 对
CentralCache热点桶进行分片。 - 降低
PageCache全局锁影响。