highpool测试报告

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. 测试方法

测试流程:

  1. 创建指定数量的工作线程。
  2. 所有线程通过条件变量同步起跑。
  3. 每个线程循环执行多轮"批量申请 -> 批量释放"。
  4. 分别统计 highpool 和 malloc/free 的耗时。
  5. 使用 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 全局锁竞争。
  • ThreadCacheCentralCache 交互过于频繁。

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 能切出来的对象数量减少。
  • 批量搬运数量下降。
  • 更容易触发 CentralCachePageCache 交互。

因此,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 线程的高并发热点场景中,当前实现还没有充分发挥多线程扩展性。

后续优化重点应放在:

  • 减少 ThreadCacheCentralCache 的交互频率。
  • 提高热点小对象的本地缓存能力。
  • CentralCache 热点桶进行分片。
  • 降低 PageCache 全局锁影响。
相关推荐
liuyao_xianhui1 小时前
进程概念与进程状态_Linux
linux·运维·服务器·数据结构·c++·哈希算法·宽度优先
迷途之人不知返1 小时前
List的模拟实现
数据结构·c++·学习·list
无敌秋2 小时前
C++ 抽象工厂模式实战指南
开发语言·c++·抽象工厂模式
CoderMeijun2 小时前
C++ 智能指针:auto_ptr
c++·内存管理·智能指针·raii·auto_ptr
wuminyu2 小时前
专家视角看Lambda表达式的原理解析
java·linux·c语言·jvm·c++
ximu_polaris2 小时前
设计模式(C++)-行为型模式-命令模式
c++·设计模式·命令模式
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 189. 轮转数组 | C++ 三次反转经典魔法 (O(1) 空间)
c++·算法·leetcode
淀粉肠kk3 小时前
【C++11】智能指针详解
开发语言·c++
不想写代码的星星3 小时前
COW(Copy-on-Write):开抄开抄,哎嘿,我装的
开发语言·c++