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 1看cs列,或者用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系统性能大打折扣。我建议你在做性能测试时,先把这四个指标跑一遍,心里就有底了。
下一章,我们聊聊内存性能瓶颈——嗯,那又是另一个故事了。