强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

06 - 性能调优

第 6 章:性能调优

6.1 调优原则

调优 jemalloc 之前,先明确两个核心指标:

指标说明优先级
吞吐量 (Throughput)每秒完成的 malloc/free 次数计算密集型任务优先
内存占用 (RSS)进程的常驻内存集内存敏感场景优先

重要:吞吐量和内存占用通常是此消彼长的关系。调优的本质是在两者之间找到最优平衡点。

调优决策树

                    开始调优
                      │
            ┌─────────▼──────────┐
            │ 当前瓶颈是什么?     │
            └────┬──────────┬────┘
                 │          │
         ┌───────▼───┐  ┌──▼──────────┐
         │ CPU 使用率高│  │ 内存占用高   │
         │ (分配慢)    │  │ (RSS 大)    │
         └───────┬───┘  └──┬──────────┘
                 │         │
    ┌────────────▼───┐  ┌──▼───────────────┐
    │ 增大 TC        │  │ 减小 dirty_decay  │
    │ 调整 Arena 数  │  │ 减小 Arena 数     │
    │ 禁用 profiling │  │ 启用 background   │
    └────────────────┘  └──────────────────┘

6.2 Arena 数量调优

为什么 Arena 数量重要

Arena 数量效果适用场景
过多元数据开销大,RSS 增加
过少锁竞争增加,吞吐量下降
适中平衡性能和内存✅ 推荐

默认值与推荐值

  • 默认4 × CPU 核心数
  • 推荐起点min(4 × ncpu, 16) 或直接 16
# 查看当前 CPU 核心数
nproc

# 对于 64 核机器,默认 256 个 Arena(可能过多)
# 推荐减少到 8-16
export MALLOC_CONF="narenas:8"

调优实验

// arena_test.c - 测试不同 Arena 数量对性能的影响
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
#include <jemalloc/jemalloc.h>

#define N_THREADS  16
#define N_ALLOCS   100000
#define OBJ_SIZE   256

static void *worker(void *arg) {
    void *ptrs[N_ALLOCS];
    for (int i = 0; i < N_ALLOCS; i++) {
        ptrs[i] = malloc(OBJ_SIZE);
        memset(ptrs[i], 0, OBJ_SIZE);
    }
    for (int i = 0; i < N_ALLOCS; i++) {
        free(ptrs[i]);
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    pthread_t threads[N_THREADS];
    struct timespec t0, t1;

    clock_gettime(CLOCK_MONOTONIC, &t0);
    for (int i = 0; i < N_THREADS; i++)
        pthread_create(&threads[i], NULL, worker, NULL);
    for (int i = 0; i < N_THREADS; i++)
        pthread_join(threads[i], NULL);
    clock_gettime(CLOCK_MONOTONIC, &t1);

    double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
    printf("narenas=%-4s  elapsed=%.3fs\n",
           argc > 1 ? argv[1] : "default", elapsed);

    return 0;
}
gcc -O2 -o arena_test arena_test.c -ljemalloc -lpthread

# 不同 Arena 数量对比
for n in 1 4 8 16 32 64; do
    MALLOC_CONF="narenas:$n" ./arena_test $n
done

典型结果(16 线程,64 核机器):

narenas=1     elapsed=2.831s   # 严重锁竞争
narenas=4     elapsed=1.204s
narenas=8     elapsed=0.856s   # 推荐
narenas=16    elapsed=0.812s   # 推荐
narenas=32    elapsed=0.835s
narenas=64    elapsed=0.891s   # 元数据开销增加

6.3 脏页回收调优

脏页与 muzzy 页

状态说明对应 madvise
Clean未使用,已归还 OS
Dirty已释放,jemalloc 仍持有
Muzzy已 madvise(MADV_FREE),OS 可在内存压力下回收MADV_FREE
进程释放内存 → Dirty 页(仍计入 RSS)
                │
                ├─ dirty_decay_ms 后 → Muzzy 页(MADV_FREE,RSS 可能降低)
                │
                └─ muzzy_decay_ms 后 → Clean 页(MADV_DONTNEED,RSS 降低)

参数调优

# 场景 1:内存敏感(容器化服务)
export MALLOC_CONF="dirty_decay_ms:1000,muzzy_decay_ms:3000"
# 特点:快速归还 OS,RSS 稳定

# 场景 2:性能优先(批处理)
export MALLOC_CONF="dirty_decay_ms:30000,muzzy_decay_ms:60000"
# 特点:减少系统调用,吞吐量更高

# 场景 3:永不变还(极端性能优化)
export MALLOC_CONF="dirty_decay_ms:-1,muzzy_decay_ms:-1"
# 特点:RSS 只增不减,但分配性能最高

手动触发回收

#include <jemalloc/jemalloc.h>

// 手动触发所有 Arena 的脏页回收
void purge_all_arenas() {
    unsigned narenas;
    size_t len = sizeof(narenas);
    je_mallctl("arenas.narenas", &narenas, &len, NULL, 0);

    for (unsigned i = 0; i < narenas; i++) {
        char cmd[64];
        snprintf(cmd, sizeof(cmd), "arena.%u.purge", i);
        je_mallctl(cmd, NULL, NULL, NULL, 0);
    }
}

// 在内存紧张时调用
void on_memory_pressure() {
    purge_all_arenas();
}

6.4 Thread Cache 调优

tcache_max 参数

# 默认 TC 上限为 32KB(某些版本为 16KB)
# 增大可提升中等大小对象的分配性能
export MALLOC_CONF="tcache_max:65536"  # 64KB

# 查看当前值
MALLOC_CONF="stats_print:true" ./my_program 2>&1 | grep tcache_max

TC 对不同大小分布的影响

对象大小分布tcache_max 建议说明
主要 < 4KB8192 (默认)默认即可
主要 4KB - 64KB65536增大 TC 覆盖更多大小类
主要 > 64KB不影响大对象不经过 TC

6.5 透明大页 (Transparent Huge Pages / THP)

背景

Linux 的透明大页 (THP) 可以将多个 4KB 页合并为 2MB 大页,减少 TLB miss,提升性能。

jemalloc 与 THP

参数说明默认值
metadata_thp元数据是否使用 THPdisabled
opt.metadata_thp元数据 THP 策略disabled

可选值:

  • disabled:不使用 THP
  • auto:有需要时使用
  • always:始终使用
# 启用元数据 THP(可减少 TLB miss)
export MALLOC_CONF="metadata_thp:auto"

# 查看系统 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never

# 验证效果
MALLOC_CONF="metadata_thp:auto,stats_print:true" ./my_program 2>&1 | grep thp

注意:THP 在某些场景下可能导致延迟抖动(kswapd compaction),建议在生产环境测试后决定是否启用。


6.6 NUMA 优化

NUMA 架构简介

┌──────────────────────┐    ┌──────────────────────┐
│     NUMA Node 0      │    │     NUMA Node 1      │
│  ┌─────────────────┐ │    │ ┌─────────────────┐  │
│  │ CPU 0-7         │ │    │ │ CPU 8-15        │  │
│  │ Local Memory    │ │ QPI│ │ Local Memory    │  │
│  └─────────────────┘ │◄──►│ └─────────────────┘  │
└──────────────────────┘    └──────────────────────┘

跨 NUMA 节点访问内存的延迟比本地访问高 2-3 倍。

jemalloc 的 NUMA 支持

jemalloc 默认的 Arena 分配策略已经可以在一定程度上实现 NUMA 本地化:

  • 线程通常被调度到某个 NUMA 节点的 CPU 上
  • 该线程使用的 Arena 的内存页会被分配到该 CPU 本地的 NUMA 节点

进一步优化

// 绑定线程到特定 NUMA 节点并使用对应 Arena
#include <pthread.h>
#include <sched.h>
#include <jemalloc/jemalloc.h>

void setup_numa_thread(int node, unsigned arena_id) {
    // 绑定 CPU
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    // 简化示例:假设 node 0 对应 CPU 0-7,node 1 对应 CPU 8-15
    for (int i = node * 8; i < (node + 1) * 8; i++) {
        CPU_SET(i, &cpuset);
    }
    pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);

    // 绑定 Arena
    je_mallctl("thread.arena", NULL, NULL, &arena_id, sizeof(arena_id));
}

NUMA 关键建议

建议说明
Arena 数 ≥ NUMA 节点数确保每个节点有独立 Arena
线程绑定 CPU确保线程不跨节点迁移
使用 numactl限制进程到特定 NUMA 节点
# 限制进程到 NUMA 节点 0
numactl --cpunodebind=0 --membind=0 ./my_server

6.7 Background Thread

作用

启用后台线程后,jemalloc 会创建专用线程来异步执行脏页回收和 TC GC,避免在业务线程的分配/释放路径上执行这些操作。

# 启用后台线程
export MALLOC_CONF="background_thread:true"

# 查看后台线程状态
MALLOC_CONF="background_thread:true,stats_print:true" ./my_program 2>&1 | grep background
参数说明默认值
background_thread启用后台线程false
max_background_threads最大后台线程数ncpu

效果

  • 减少分配延迟抖动:脏页回收不再阻塞 malloc/free
  • CPU 开销转移:从分配路径转移到后台线程
  • 适合延迟敏感场景:如实时系统、API 服务
# 适合高并发低延迟服务
export MALLOC_CONF="background_thread:true,dirty_decay_ms:2000"

6.8 调优实验框架

// tune_bench.c - 系统化调优测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
#include <jemalloc/jemalloc.h>

typedef struct {
    int id;
    int n_allocs;
    int obj_size_min;
    int obj_size_max;
} thread_arg_t;

static void *bench_thread(void *arg) {
    thread_arg_t *a = (thread_arg_t *)arg;
    void **ptrs = malloc(a->n_allocs * sizeof(void *));

    for (int i = 0; i < a->n_allocs; i++) {
        size_t sz = a->obj_size_min +
                    rand() % (a->obj_size_max - a->obj_size_min + 1);
        ptrs[i] = malloc(sz);
        if (ptrs[i]) memset(ptrs[i], 0xAB, sz);
    }
    for (int i = 0; i < a->n_allocs; i++) {
        free(ptrs[i]);
    }
    free(ptrs);
    return NULL;
}

int main(int argc, char *argv[]) {
    int n_threads = argc > 1 ? atoi(argv[1]) : 8;
    int n_allocs  = argc > 2 ? atoi(argv[2]) : 50000;

    pthread_t *threads = malloc(n_threads * sizeof(pthread_t));
    thread_arg_t *args = malloc(n_threads * sizeof(thread_arg_t));

    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);

    for (int i = 0; i < n_threads; i++) {
        args[i] = (thread_arg_t){i, n_allocs, 16, 4096};
        pthread_create(&threads[i], NULL, bench_thread, &args[i]);
    }
    for (int i = 0; i < n_threads; i++) {
        pthread_join(threads[i], NULL);
    }

    clock_gettime(CLOCK_MONOTONIC, &t1);
    double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
    double ops = (double)n_threads * n_allocs * 2 / elapsed;

    printf("threads=%d allocs=%d elapsed=%.3fs ops=%.0f/s\n",
           n_threads, n_allocs, elapsed, ops);

    free(threads);
    free(args);
    return 0;
}
gcc -O2 -o tune_bench tune_bench.c -ljemalloc -lpthread

# 测试脚本
cat << 'SCRIPT' > tune_test.sh
#!/bin/bash
echo "=== Arena Count Test ==="
for n in 1 4 8 16 32; do
    printf "narenas=%-3d " $n
    MALLOC_CONF="narenas:$n" ./tune_bench 16 50000
done

echo ""
echo "=== Dirty Decay Test ==="
for ms in 0 1000 5000 30000 -1; do
    printf "dirty_decay=%-6d " $ms
    MALLOC_CONF="dirty_decay_ms:$ms" ./tune_bench 16 50000
done

echo ""
echo "=== Background Thread Test ==="
for bt in true false; do
    printf "bg_thread=%-5s " $bt
    MALLOC_CONF="background_thread:$bt" ./tune_bench 16 50000
done
SCRIPT
chmod +x tune_test.sh
./tune_test.sh

6.9 调优参数速查表

参数调优方向推荐值范围
narenas减少 → 省内存;增多 → 提高并发4-16
dirty_decay_ms减少 → 省内存;增大 → 提高性能1000-30000
muzzy_decay_ms减少 → 省内存;增大 → 提高性能5000-60000
tcache_max增大 → 中等对象更快16KB-128KB
background_thread开启 → 降低延迟抖动true/false
metadata_thp开启 → 减少 TLB missauto/always/disabled

6.10 本章小结

调优目标关键参数
降低延迟background_thread:true, 较大 tcache_max
降低内存减小 narenas, 减小 dirty_decay_ms
提高吞吐增大 narenas, 增大 dirty_decay_ms, 关闭 profiling
NUMA 优化Arena 数 ≥ NUMA 节点数 + CPU 绑定

扩展阅读

  1. jemalloc Tuning Guide
  2. Linux NUMA 优化
  3. 透明大页 (THP) 文档

上一章第 5 章:内存分析与 Profiling 下一章第 7 章:系统集成