3、CPU性能瓶颈:上下文切换、中断处理、缓存命中率、CPU亲和性

好,我们接着聊CPU性能瓶颈。说实话,在ADAS系统里,CPU往往是第一个喊累的。你想想看,摄像头数据哗哗地进来,感知模块要算,规划模块要算,控制模块也要算。CPU要是扛不住,整个系统就卡在那了。

我个人习惯,排查CPU性能问题,先看四个维度:上下文切换、中断处理、缓存命中率、CPU亲和性。这四个点,任何一个出问题,都能让你的系统性能直接腰斩。

3.1 上下文切换:看不见的隐形杀手

什么是上下文切换?说白了,就是CPU从一个任务切换到另一个任务时,要保存当前任务的状态,再加载新任务的状态。这个过程是有代价的——它要花时间,要清缓存,要刷新TLB。

我在项目中遇到过这样一个案例:某款ADAS控制器,感知模块的延迟突然从10ms飙到了30ms。一开始我以为是算法变慢了,结果用perf一查,发现上下文切换次数高得离谱——每秒超过10万次。

为什么会这样?因为系统里跑着太多线程了。每个线程都在抢CPU时间片,CPU疲于奔命地做切换,真正干活的时间反而少了。

关键指标:

  • 每秒上下文切换次数 < 5000:正常
  • 5000 ~ 20000:需要关注
  • > 20000:大概率出问题了

排查方法很简单,用vmstat 1cs列,或者用pidstat -w看每个进程的切换情况。

# 查看系统上下文切换
vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 123456  1234  56789    0    0    10    20  500 12000 30 20 50  0  0

你看这个cs列,12000次/秒,已经偏高了。我当时那个项目,就是通过减少不必要的线程、合并同类任务,把上下文切换降到了3000次/秒以下,延迟问题迎刃而解。

我的建议:在ADAS系统里,线程数不要超过CPU核心数的2倍。多了就是浪费,反而拖慢系统。

3.2 中断处理:别让CPU疲于奔命

中断,是硬件通知CPU有事要处理的一种机制。比如摄像头数据来了,网卡收到报文了,都会触发中断。

中断本身没问题,问题是中断太多了。我记得有一次,客户反馈说系统在高速行驶时,控制指令的响应延迟忽高忽低。我排查了半天,发现是CAN总线中断太频繁了——每毫秒触发一次,CPU被频繁打断,正经事反而干不了。

中断处理分两种:

  • 上半部(top half):快速响应,只做最必要的事,比如把数据拷贝到缓冲区
  • 下半部(bottom half):延迟处理,比如数据解析、协议处理

嗯,这里要注意:上半部要尽量短,否则会阻塞其他中断。我曾经见过一个驱动,上半部里做了内存分配,结果导致中断延迟飙升——这绝对是坑。

避坑指南:我曾经在调试一个雷达驱动时,发现中断处理函数里调用了printk。你知道printk有多慢吗?它会把整个中断处理拖垮。记住:中断上半部里,绝对不要做任何可能阻塞的操作。

排查中断问题,用/proc/interrupts看每个中断的触发次数:

cat /proc/interrupts | grep -E "CPU|CAN"
           CPU0       CPU1       CPU2       CPU3
 56:     123456     234567     345678     456789  GIC-0  CAN0
 57:     789012     890123     901234     123456  GIC-0  CAN1

如果某个中断的触发次数异常高,就要考虑是不是中断合并(interrupt coalescing)没做好,或者硬件配置有问题。

3.3 缓存命中率:CPU的隐形加速器

CPU缓存,说白了就是CPU和内存之间的一个高速缓冲区。L1缓存最快,但只有几十KB;L2稍慢,几百KB;L3更慢,几MB。如果数据在缓存里,CPU访问只需要几个时钟周期;如果不在,就要去内存里拿,那就要上百个时钟周期了。

你想想看,如果缓存命中率低,CPU大部分时间都在等内存,性能能好吗?

我在项目中遇到过这样一个场景:感知模块的某个算法,在测试平台上跑得好好的,一上实车就慢了一倍。我一开始以为是硬件差异,后来用perf stat一看,发现L1缓存缺失率从5%飙升到了30%。

为什么会这样?因为测试平台的数据是连续分配的,而实车上的数据因为内存碎片,分布得很散。CPU每次访问都要去内存里拿,自然就慢了。

关键指标:

缓存级别 命中率目标 缺失代价
L1 > 95% ~10个时钟周期
L2 > 90% ~50个时钟周期
L3 > 80% ~200个时钟周期

排查方法:

# 用perf stat查看缓存命中率
perf stat -e L1-dcache-loads,L1-dcache-load-misses ./your_app

# 输出示例
     1,234,567,890      L1-dcache-loads
       123,456,789      L1-dcache-load-misses   # 10% 缺失率,偏高

优化缓存命中率,我常用的方法:

  • 数据局部性:把经常一起访问的数据放在一起,比如结构体里按访问频率排列成员
  • 预取:__builtin_prefetch提前把数据加载到缓存
  • 对齐:确保关键数据结构按缓存行(64字节)对齐

我的经验:在ADAS系统里,图像数据特别容易造成缓存缺失。因为图像是二维的,而内存是一维的。遍历图像时,按行访问比按列访问快得多——这就是缓存局部性的威力。

3.4 CPU亲和性:把任务绑在正确的核上

CPU亲和性,说白了就是让某个任务固定在某个CPU核心上运行。为什么要这么做?因为如果不绑定,任务会在不同核心之间来回迁移,每次迁移都要清缓存、刷新TLB,性能损失很大。

我记得有一次,系统里跑着三个感知算法,每个算法都分配了独立的CPU核心。但因为没有设置亲和性,操作系统把任务调度到了不同的核心上,结果三个算法互相干扰,延迟都变高了。

设置CPU亲和性,用taskset或者sched_setaffinity

# 把进程绑定到CPU0和CPU1
taskset -c 0,1 ./perception_node

# 在代码里设置亲和性
#include <sched.h>
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);  // 绑定到CPU0
sched_setaffinity(0, sizeof(cpuset), &cpuset);

在ADAS系统里,我一般这样分配:

  • CPU0:运行实时控制任务,比如CAN通信、执行器控制
  • CPU1-2:运行感知算法,比如目标检测、车道线识别
  • CPU3:运行规划决策模块
  • 中断:尽量绑定到非实时核心上,避免打断控制任务

注意:我曾经犯过一个错误——把两个高负载的感知算法绑到了同一个核心上。结果两个算法互相抢CPU时间,延迟都变高了。记住:亲和性不是万能的,核心负载要均衡。

排查亲和性问题,用ps -eo pid,comm,psr看每个进程运行在哪个核心上:

ps -eo pid,comm,psr | grep perception
 1234 perception_node   0
 1235 perception_node   1
 1236 perception_node   0  # 这个应该绑到2号核心的!

如果发现任务在核心之间频繁迁移,就要检查亲和性设置是否正确。

小结

好了,CPU性能瓶颈这块,我总结一下:

  • 上下文切换:别让CPU疲于奔命,控制线程数量
  • 中断处理:上半部要短,下半部要快,别在中断里干重活
  • 缓存命中率:数据局部性是王道,按行访问比按列访问快得多
  • CPU亲和性:把任务绑在正确的核上,别让它们乱跑

这四个点,任何一个出问题,都会让你的ADAS系统性能大打折扣。我建议你在做性能测试时,先把这四个指标跑一遍,心里就有底了。

下一章,我们聊聊内存性能瓶颈——嗯,那又是另一个故事了。