3. 性能瓶颈分析:CPU占用率、内存泄漏、I/O等待、上下文切换
仿真跑得慢,大家第一反应往往是「加机器」。但说实话,很多时候问题不在硬件,而在软件本身。我见过太多团队,一遇到仿真卡顿就申请更高配的服务器,结果换了机器还是卡。为什么?因为根本没找到真正的瓶颈。
这一章,我们就来聊聊SITL仿真中四个最常见的性能杀手:CPU占用率、内存泄漏、I/O等待、上下文切换。每个我都会结合项目中的实际案例来讲,希望能帮你少走一些弯路。
3.1 CPU占用率:仿真主循环的「心跳」
CPU占用率是最直观的性能指标。但很多人只看一个数字:CPU是不是100%了?其实没那么简单。
我个人习惯把CPU占用率分成两类来看:
- 用户态CPU(user):仿真代码本身在跑,比如物理引擎计算、传感器模型更新。
- 系统态CPU(sys):内核在干活,比如线程调度、中断处理、系统调用。
如果user高,说明你的仿真逻辑太重。如果sys高,那就要小心了——可能是系统调用太频繁,或者线程数太多。
top 或 htop 看CPU时,按「1」键展开每个核心。如果某个核心跑满而其他核心空闲,说明你的仿真程序是单线程瓶颈。这时候加CPU核心数没用,得优化算法或做并行化。
举个例子。我之前优化一个四旋翼SITL仿真,发现CPU占用率只有30%,但仿真就是跑不快。用perf top一看,好家伙,40%的CPU时间花在了clock_gettime这个系统调用上。原来是代码里每个循环都调用了高精度时钟,用来做时间同步。后来改成每100个循环才同步一次,CPU占用率直接降到8%,仿真帧率翻了一倍。
# 用 perf 定位 CPU 热点
$ perf top -p <pid>
# 输出示例
# 40.12% [kernel] [k] clock_gettime
# 15.34% [sim] [.] physics_step
# 10.21% [sim] [.] sensor_update
# 8.45% [libc] [.] __write
3.2 内存泄漏:温水煮青蛙
内存泄漏在SITL仿真中特别隐蔽。为什么?因为仿真程序通常跑几个小时甚至几天,泄漏一点点看不出来。等跑了一整天,内存从200MB涨到2GB,系统开始swap,仿真就突然卡死了。
我曾经接手过一个无人机编队仿真项目,8架飞机同时仿真,跑2小时后必崩。一开始以为是网络问题,排查了两天没结果。后来用valgrind一查,发现是传感器模型里每次生成新数据都malloc了一块内存,但从来没free过。8架飞机,每架每秒生成100条数据,2小时就是...嗯,你算算。
排查内存泄漏,我常用的工具链:
| 工具 | 适用场景 | 我的用法 |
|---|---|---|
| valgrind --leak-check=full | 定位泄漏的代码位置 | 跑一小段仿真,看报告中的「definitely lost」 |
| heaptrack | 分析内存分配热点 | 生成火焰图,看哪些函数分配最多 |
| /proc/<pid>/smaps | 实时监控进程内存 | 写脚本每10秒采样,看RSS是否持续增长 |
| address sanitizer (ASan) | 编译时检测内存错误 | 加 -fsanitize=address 重新编译 |
/proc/smaps确认有泄漏,再用valgrind跑小规模场景定位代码。别一上来就上重武器。
3.3 I/O等待:被忽视的隐形杀手
I/O等待(iowait)是CPU花在等待磁盘或网络I/O完成上的时间。很多人看到CPU占用率不高,但仿真就是卡,第一反应是「加内存」。其实看看iostat,可能发现磁盘利用率已经100%了。
SITL仿真中常见的I/O瓶颈:
- 日志写入太频繁:每帧都写日志,磁盘根本跟不上。
- 加载模型文件:每次仿真启动都要读几百MB的3D模型或地图数据。
- 网络通信:和地面站、其他仿真节点之间的UDP/TCP通信。
- swap:内存不够时,系统把内存换到磁盘,那速度...惨不忍睹。
我记得有一次,一个客户说他们的SITL仿真每30秒就卡顿一次,持续2-3秒。我上去一看iostat -x 1,发现磁盘utilization每30秒跳到100%,持续几秒。再查,原来是日志系统每30秒做一次缓冲区刷新,把累积的日志一次性写入磁盘。改成异步写入+更小的缓冲区后,卡顿消失了。
# 监控 I/O 等待
$ iostat -x 1
# 关注 %iowait 和 await 列
# 如果 %iowait > 10% 且 await > 100ms,说明磁盘是瓶颈
# 查看具体进程的 I/O
$ iotop -oP
3.4 上下文切换:线程太多的代价
上下文切换,说白了就是CPU从一个线程切换到另一个线程时,要保存和恢复状态。这个操作本身有开销。如果切换太频繁,CPU就在「保存-恢复-保存-恢复」中空转,真正干活的时间反而少了。
怎么看上下文切换频率?用vmstat或者pidstat -w:
# 每秒报告一次上下文切换
$ vmstat 1
# 关注 cs(context switch)列
# 如果 cs > 100000 次/秒,说明切换太频繁了
# 看具体进程
$ pidstat -w -p <pid> 1
我见过最夸张的一个案例:一个SITL仿真程序开了200多个线程,每个线程只做一点点事。结果上下文切换占了CPU的60%以上。优化方案很简单——用线程池,把200个线程合并成8个。仿真速度直接提升了3倍。
什么时候上下文切换会成为问题?
- 线程数远大于CPU核心数:比如8核CPU开了64个线程。
- 锁竞争激烈:线程之间频繁争抢同一个锁,导致大量阻塞和唤醒。
- 时间片太短:每个线程只跑几微秒就被切换出去。
3.5 综合诊断:一个实际案例
说了这么多,来一个完整的诊断流程。假设你遇到一个SITL仿真跑得慢,怎么办?
我一般按这个顺序排查:
- 先看整体负载:
top+vmstat,看CPU、内存、I/O、上下文切换的概览。 - 定位具体进程:
pidstat看哪个进程在吃资源。 - 深入分析:如果是CPU高,用
perf;如果是内存,用valgrind;如果是I/O,用iostat;如果是上下文切换,用pidstat -w。 - 验证修复:改完代码后,重复步骤1-3,确认瓶颈消失。
举个例子。有一次我优化一个多机协同仿真,8架无人机,跑起来CPU占用率80%,但仿真帧率只有10Hz(目标30Hz)。
第一步,top一看,CPU 80%里user占60%,sys占20%。sys偏高,说明系统调用或线程调度有问题。
第二步,pidstat -w一看,上下文切换每秒30万次。8核CPU,30万次切换,明显太多了。
第三步,perf top一看,热点在pthread_mutex_lock和futex。原来是每架飞机的仿真线程都在争抢同一个全局锁——日志缓冲区锁。
第四步,改成每架飞机独立的日志缓冲区,再加一个后台线程统一写入。上下文切换降到5万次/秒,CPU占用率降到40%,仿真帧率稳定在30Hz。