10、内存池性能测试:基准测试方法、吞吐量与延迟测量、与malloc对比
性能测试,说白了就是拿数据说话。
你设计了一个内存池,自我感觉良好。但到底快不快?快了多少?在什么场景下快?这些问题,光靠猜是不行的。我见过不少开发者,花了两周优化内存池,结果一测,跟 malloc 差不多,甚至更慢。嗯,这就是没做基准测试的后果。
10.1 基准测试的核心思路
基准测试不是随便跑两下就完事。我个人习惯,先明确三个问题:
- 测什么?——分配+释放的完整周期,还是只测分配?
- 怎么测?——单线程还是多线程?固定大小还是随机大小?
- 对比谁?——glibc malloc、tcmalloc、jemalloc?
我曾经犯过一个错误:只测了分配速度,没测释放。结果内存池分配确实快,但释放时因为要合并空闲块,反而比 malloc 慢。所以,一定要测完整周期。
核心原则:测试环境要稳定。关掉 CPU 频率缩放、关掉 ASLR、固定 CPU 亲和性。否则你测出来的数据,噪声比信号还大。
10.2 吞吐量(Throughput)测量
吞吐量,就是单位时间内能完成多少次分配/释放操作。单位通常是 ops/s(操作每秒)。
怎么测?我一般用这样的方法:
- 预分配一批内存块(比如 10000 个)
- 循环执行:分配 → 写入 → 释放
- 记录总耗时,计算吞吐量
// 吞吐量测试示例
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ITERATIONS 1000000
#define BLOCK_SIZE 64
double test_throughput(void *(*alloc)(size_t), void (*dealloc)(void *)) {
clock_t start = clock();
for (int i = 0; i < ITERATIONS; i++) {
void *p = alloc(BLOCK_SIZE);
if (p) {
// 模拟写入,防止编译器优化掉分配
*(volatile char *)p = 0;
dealloc(p);
}
}
clock_t end = clock();
double elapsed = (double)(end - start) / CLOCKS_PER_SEC;
return ITERATIONS / elapsed; // ops/s
}
小技巧:用
volatile 防止编译器把分配优化掉。我见过有人没加这个,结果编译器发现分配的内存没使用,直接把整个循环优化没了——测出来的吞吐量是无穷大,哈哈。
10.3 延迟(Latency)测量
延迟,就是单次分配/释放操作需要多长时间。单位通常是 ns(纳秒)或 us(微秒)。
吞吐量和延迟,其实是同一枚硬币的两面。但延迟更关注「最坏情况」。你想想看,一个内存池平均延迟 50ns,但偶尔一次飙到 10us——这对实时系统来说就是灾难。
测量延迟,我推荐用 rdtsc 指令(x86 平台),精度比 clock_gettime 高得多:
// 延迟测量示例(x86)
static inline uint64_t rdtsc() {
uint32_t lo, hi;
__asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}
void test_latency() {
uint64_t min = UINT64_MAX;
uint64_t max = 0;
uint64_t sum = 0;
for (int i = 0; i < 10000; i++) {
uint64_t start = rdtsc();
void *p = my_pool_alloc(64);
uint64_t end = rdtsc();
uint64_t latency = end - start;
if (latency < min) min = latency;
if (latency > max) max = latency;
sum += latency;
my_pool_free(p);
}
printf("Min: %lu cycles, Max: %lu cycles, Avg: %lu cycles\n",
min, max, sum / 10000);
}
注意:rdtsc 在不同 CPU 核心之间可能不同步。如果你用多线程测试,建议绑定到同一个核心。我曾经被这个坑过——两个核心的 TSC 差了 0.1%,测出来的延迟数据全是错的。
10.4 与 malloc 对比
对比测试,要控制变量。我一般这样设计:
| 测试场景 | 内存池 | malloc | 提升倍数 |
|---|---|---|---|
| 固定大小(64B)单线程 | 85 ns | 120 ns | 1.4x |
| 固定大小(64B)多线程 | 92 ns | 350 ns | 3.8x |
| 随机大小(16-1024B) | 110 ns | 180 ns | 1.6x |
| 大块(4KB) | 200 ns | 150 ns | 0.75x |
看到了吗?内存池不是万能的。大块分配时,malloc 反而更快。为什么?因为 malloc 底层用 mmap 直接映射大页,而内存池还要经过自己的管理逻辑。
所以我的建议是:内存池适合小对象、高频分配的场景。大对象或者低频分配,直接用 malloc 就好。
10.5 知识体系图
下面这张图,概括了内存池性能测试的完整流程:
10.6 常见陷阱与避坑指南
- 预热问题:刚启动时,CPU 缓存是冷的。我一般先跑 10000 次「热身」,再开始正式测量。
- 编译器优化:分配的内存如果不用,编译器可能直接跳过。用
volatile或者把指针传给一个外部函数。 - 系统噪声:后台进程、中断、调度器都会影响结果。我习惯跑 10 次取中位数,而不是平均值。
- 内存对齐:有些平台不对齐会触发异常。测试时记得检查返回地址的对齐情况。
我的经验:写测试代码时,把被测函数指针作为参数传进去。这样同一套测试代码,可以测内存池、测 malloc、测 tcmalloc——换一个参数就行,代码不用改。
好了,关于性能测试,就聊这么多。记住一句话:没有测量,就没有优化。你觉得自己快,不算数;跑出数据来,才算数。
公众号:蓝海资料掘金营,微信deep3321