CPU性能优化:缓存、上下文切换与亲和性

CPU 优化,说白了就是让处理器少等、少切换、多干活。我做了这么多年性能调优,发现大部分性能问题其实都卡在 CPU 的“等待”上——等内存、等调度、等缓存同步。今天咱们就把这几个核心问题拆开聊。

CPU缓存原理:为什么你的程序跑不快

先问个问题:CPU 主频已经到 3GHz 以上,内存访问却还在 100ns 级别。你想想看,CPU 执行一条指令只要 0.3ns,等一次内存访问却要等 300 多个指令周期。这中间的巨大差距,全靠缓存来填。

现代 CPU 通常有三层缓存:

缓存层级 典型大小 访问延迟 缓存行大小
L1 32KB - 64KB ~1ns (3-4 cycles) 64 字节
L2 256KB - 512KB ~4ns (10-12 cycles) 64 字节
L3 8MB - 32MB ~15ns (40-50 cycles) 64 字节

这里有个关键概念——缓存行。CPU 从内存读数据,一次就读 64 字节。哪怕你只用一个 int,它也会把相邻的 64 字节全拉进缓存。

核心原则:利用空间局部性,让数据在缓存行内连续访问。一次缓存未命中(cache miss)的代价,大约是命中时的 10-20 倍。

我在项目中遇到过这样一个案例:一个日志处理程序,每秒处理 10 万条记录。每条记录有 8 个字段,结构体定义是乱序的。我建议把热点字段放在结构体头部,让它们落在同一个缓存行里。结果吞吐量直接提升了 40%。

实战技巧:写多线程程序时,注意“伪共享”(False Sharing)。两个线程各自修改不同变量,但这两个变量恰好在同一个缓存行里——每次修改都会导致对方的缓存行失效。解决方案:在变量之间填充 64 字节的 padding。

上下文切换:看不见的性能杀手

上下文切换,就是操作系统把 CPU 从一个线程切换到另一个线程。每次切换都要保存当前线程的寄存器、程序计数器、栈指针,再加载新线程的上下文。这个开销有多大?

  • 直接开销:保存/恢复寄存器,约 1-5μs
  • 间接开销:缓存污染。新线程的数据不在缓存里,需要重新加载
  • TLB 失效:页表缓存被清空,后续内存访问变慢

我曾经在一个高并发网关项目里,发现 CPU 利用率只有 30%,但吞吐量上不去。用 perf 一看,上下文切换频率高达每秒 20 万次。每个请求处理不到 100μs,切换开销占了 30%。

避坑指南:我曾经以为线程越多越好,结果 8 核机器开了 64 个线程,性能反而下降。后来用 vmstat 1 观察 cs(context switch)列,发现每秒切换超过 5 万次时,性能就开始倒挂。建议:活跃线程数不超过 CPU 核心数的 2 倍。

怎么排查上下文切换问题?

# 查看系统级上下文切换
vmstat 1

# 查看每个进程的上下文切换
pidstat -w 1

# 用 perf 统计切换原因
perf stat -e context-switches,cpu-migrations ./your_app

嗯,这里要注意:cpu-migrations 表示线程在不同 CPU 核心之间迁移。这种迁移比普通上下文切换更贵,因为它会导致 L1/L2 缓存完全失效。

CPU绑定与亲和性:把线程钉在核心上

默认情况下,操作系统会把线程调度到任意空闲核心上。这听起来很公平,但代价就是缓存不断被冲垮。CPU 亲和性(affinity)就是告诉内核:这个线程只跑在指定的核心上。

我个人习惯把 CPU 亲和性分成两种场景:

场景 做法 收益
计算密集型 每个线程绑定到独立物理核 缓存命中率提升 20-50%
I/O 密集型 绑定到同一核或相邻核 减少核间通信开销
中断处理 绑定到专用核(isolcpus) 避免中断打断业务线程

代码里怎么设置?

// C 语言:使用 pthread_setaffinity_np
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);  // 绑定到 CPU 0
pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);

// 命令行:使用 taskset
taskset -c 0,1 ./myapp   # 绑定到 CPU 0 和 1

// 容器环境:cgroup 限制
echo "0-3" > /sys/fs/cgroup/cpuset/mygroup/cpuset.cpus

经验之谈:绑定 CPU 时,注意区分物理核和逻辑核(超线程)。两个逻辑核共享同一个物理核的执行单元,如果绑定到同一个物理核的两个逻辑核上,反而会竞争资源。用 lscpu -e 查看拓扑结构。

负载均衡:让每个核心都忙起来

CPU 绑定和负载均衡看起来矛盾——一个要固定,一个要分散。其实它们解决的是不同层面的问题。

负载均衡的目标:让所有核心的利用率尽量接近,避免有的核心忙死、有的核心闲死。

Linux 内核的负载均衡机制:

  • tick 级均衡:每个时钟中断(通常 250Hz)检查一次,把任务从忙核迁移到闲核
  • 唤醒时均衡:线程被唤醒时,选择负载最轻的核心
  • 空闲时均衡:核心空闲时,主动从其他核心拉取任务

我遇到过一个问题:一个 32 核机器,跑 Nginx 负载均衡,结果 CPU 利用率分布极不均匀——核心 0 跑到 95%,核心 31 只有 20%。原因是网卡中断全部绑定到了核心 0。

解决方案:

# 查看当前中断绑定
cat /proc/interrupts | grep eth0

# 设置中断亲和性(RPS/RFS)
echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 或者使用 irqbalance 服务自动均衡
systemctl start irqbalance

注意:过度均衡也有问题。如果线程频繁迁移,缓存命中率会急剧下降。我建议:对延迟敏感的服务,使用 CPU 绑定 + 手动分区;对吞吐量敏感的服务,让内核自动均衡。

知识体系总览

下面这张图把 CPU 性能优化的几个核心维度串起来了:

CPU 性能优化核心知识体系 CPU 性能优化 CPU 缓存原理 上下文切换 CPU 绑定与亲和性 负载均衡 缓存行 & 伪共享 空间局部性 切换开销分析 perf 工具排查 taskset / cgroup 物理核 vs 逻辑核 中断亲和性 RPS/RFS 均衡 💡 优化优先级建议 1. 先排查上下文切换是否过高(vmstat 看 cs 列) 2. 再检查缓存命中率(perf stat -e cache-misses) 3. 最后根据业务场景决定绑定还是均衡

这张图把四个核心维度以及它们之间的关联关系展示清楚了。你可以看到,缓存原理是底层基础,上下文切换是运行时开销,CPU 绑定是控制手段,负载均衡是全局策略。实际优化时,往往需要组合使用。

最后说一句:CPU 优化没有银弹。我见过太多人一上来就调内核参数、绑核、改调度器,结果问题根本不在 CPU 上。先用量化工具找到瓶颈,再对症下药——这才是靠谱的做法。