10、内存池性能测试:基准测试方法、吞吐量与延迟测量、与malloc对比

性能测试,说白了就是拿数据说话。

你设计了一个内存池,自我感觉良好。但到底快不快?快了多少?在什么场景下快?这些问题,光靠猜是不行的。我见过不少开发者,花了两周优化内存池,结果一测,跟 malloc 差不多,甚至更慢。嗯,这就是没做基准测试的后果。

10.1 基准测试的核心思路

基准测试不是随便跑两下就完事。我个人习惯,先明确三个问题:

  • 测什么?——分配+释放的完整周期,还是只测分配?
  • 怎么测?——单线程还是多线程?固定大小还是随机大小?
  • 对比谁?——glibc malloc、tcmalloc、jemalloc?

我曾经犯过一个错误:只测了分配速度,没测释放。结果内存池分配确实快,但释放时因为要合并空闲块,反而比 malloc 慢。所以,一定要测完整周期

核心原则:测试环境要稳定。关掉 CPU 频率缩放、关掉 ASLR、固定 CPU 亲和性。否则你测出来的数据,噪声比信号还大。

10.2 吞吐量(Throughput)测量

吞吐量,就是单位时间内能完成多少次分配/释放操作。单位通常是 ops/s(操作每秒)。

怎么测?我一般用这样的方法:

  1. 预分配一批内存块(比如 10000 个)
  2. 循环执行:分配 → 写入 → 释放
  3. 记录总耗时,计算吞吐量
// 吞吐量测试示例
#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 知识体系图

下面这张图,概括了内存池性能测试的完整流程:

内存池性能测试知识体系 基准测试方法 吞吐量测量 延迟测量 单线程 多线程 固定大小 随机大小 ops/s MB/s 分配+释放 仅分配 平均延迟 最大延迟 P99 延迟 抖动分析 与 malloc 对比分析

10.6 常见陷阱与避坑指南

  • 预热问题:刚启动时,CPU 缓存是冷的。我一般先跑 10000 次「热身」,再开始正式测量。
  • 编译器优化:分配的内存如果不用,编译器可能直接跳过。用 volatile 或者把指针传给一个外部函数。
  • 系统噪声:后台进程、中断、调度器都会影响结果。我习惯跑 10 次取中位数,而不是平均值。
  • 内存对齐:有些平台不对齐会触发异常。测试时记得检查返回地址的对齐情况。
我的经验:写测试代码时,把被测函数指针作为参数传进去。这样同一套测试代码,可以测内存池、测 malloc、测 tcmalloc——换一个参数就行,代码不用改。

好了,关于性能测试,就聊这么多。记住一句话:没有测量,就没有优化。你觉得自己快,不算数;跑出数据来,才算数。


公众号:蓝海资料掘金营,微信deep3321