第三章 代码性能分析:性能瓶颈识别、Profiling工具使用与时间测量方法

做运动控制,说白了就是跟时间赛跑。你写的代码再漂亮,跑起来慢了半拍,电机就该抖了。我见过太多工程师,代码逻辑天衣无缝,一上示波器,周期抖动大得吓人。嗯,今天我们就来聊聊,怎么给代码做个体检,找出那些拖后腿的“坏家伙”。

3.1 性能瓶颈识别:先找“慢”在哪

我个人习惯,拿到一个实时控制项目,不会急着优化。先问自己三个问题:

  • 是计算太慢? 比如浮点运算、三角函数、PID迭代。
  • 是访问太慢? 比如频繁读Flash、外部RAM、或者DMA没用好。
  • 是调度太慢? 比如中断嵌套、任务切换、或者锁竞争。

你想想看,如果连瓶颈在哪都不知道,上来就改代码,大概率是白费功夫。我在项目中遇到过一位同事,花了三天优化一个滤波算法,结果发现是SPI通信的等待时间占了80%。

核心原则: 先测量,后优化。没有数据支撑的优化,都是耍流氓。

3.2 Profiling工具使用:Perf与Tracealyzer

工欲善其事,必先利其器。这里我重点讲两个工具,一个轻量级,一个重量级。

3.2.1 Perf:Linux下的轻骑兵

如果你的运动控制器跑在Linux或RT-Linux上,Perf是你的首选。它不需要改代码,直接采样CPU的PC指针,就能告诉你哪个函数最耗CPU。

# 采样10秒,记录调用栈
perf record -g -F 1000 -- ./your_control_app

# 查看热点函数
perf report

我曾经用Perf抓到一个“幽灵”函数,一个看似无害的printf,在实时循环里每秒打印几千次,直接把CPU吃满了。去掉之后,周期抖动从50us降到了5us。

小技巧:perf top可以实时查看CPU占用,适合快速定位“谁在偷懒”。

3.2.2 Tracealyzer:RTOS的显微镜

对于FreeRTOS、ThreadX这类RTOS,Tracealyzer是神器。它能记录每个任务的运行状态、切换时间、中断响应时间。说白了,就是给你一张“时间轴全景图”。

我记得有一次,系统偶尔出现一次“卡顿”,用示波器都抓不到。后来用Tracealyzer一跑,发现是一个低优先级任务在某个条件下,持有了一个信号量不放,导致高优先级任务被阻塞。这种“优先级反转”问题,光看代码很难发现。

注意: Tracealyzer会占用一些资源,生产环境慎用。建议在调试阶段使用,或者只开启关键路径的跟踪。

3.3 时间测量方法:DWT与定时器

工具再好,有时候不如自己动手量一下。尤其是在裸机或者没有Profiling工具的环境下,时间测量是基本功。

3.3.1 DWT:Cortex-M内核的隐藏技能

很多工程师不知道,Cortex-M3/M4/M7内核里有一个Data Watchpoint and Trace (DWT) 模块。它里面有个CYCCNT计数器,每个时钟周期加1。用它来测量时间,精度高,开销小。

// 初始化DWT
#define DWT_CYCCNT  *(volatile uint32_t *)0xE0001004
#define DWT_CONTROL *(volatile uint32_t *)0xE0001000
#define SCB_DEMCR   *(volatile uint32_t *)0xE000EDFC

void DWT_Init(void) {
    SCB_DEMCR |= 0x01000000;  // 使能DWT
    DWT_CONTROL |= 1;         // 使能CYCCNT
    DWT_CYCCNT = 0;
}

// 测量函数执行时间
uint32_t start = DWT_CYCCNT;
your_function();
uint32_t end = DWT_CYCCNT;
uint32_t cycles = end - start;  // 消耗的时钟周期数

我个人习惯,在关键中断服务函数(ISR)的入口和出口各放一个DWT测量点。如果发现某个ISR执行时间超过了100个时钟周期,我就会警觉起来。

避坑指南: 我曾经在测量时忘记考虑中断嵌套。如果高优先级中断打断了测量,结果会偏大。建议在测量前关闭全局中断,或者使用DWT的溢出中断来处理。

3.3.2 通用定时器:万金油方案

如果芯片不支持DWT,或者你想测量更长时间(比如几秒),用通用定时器最靠谱。配置一个定时器,让它自由运行,然后读取CNT寄存器。

// 假设定时器时钟为100MHz,预分频为1000,则每个tick为10us
TIM_HandleTypeDef htim2;

void TIM_Init(void) {
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 1000 - 1;  // 100MHz / 1000 = 100kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 0xFFFFFFFF;   // 最大计数值
    HAL_TIM_Base_Init(&htim2);
    HAL_TIM_Base_Start(&htim2);
}

uint32_t GetTimeUs(void) {
    return __HAL_TIM_GET_COUNTER(&htim2) * 10;  // 单位:微秒
}

你想想看,有了这个GetTimeUs函数,你就可以在代码里到处“埋点”,测量任意一段代码的执行时间。我一般会在控制循环的头部和尾部各放一个,然后通过串口或者共享内存把数据发出去,在上位机画个波形图,一目了然。

注意: 定时器读取本身也有开销,大约几十纳秒。对于微秒级的测量,这个误差可以忽略。但如果测量纳秒级操作,建议用DWT。

3.4 知识体系总览

下面这张图,是我自己总结的“性能分析三板斧”。你可以把它当作一个检查清单,每次遇到性能问题,按这个流程走一遍,基本不会漏。

性能分析三板斧 发现性能问题 Profiling工具 时间测量 代码审查 Perf (Linux) Tracealyzer (RTOS) SystemView (SEGGER) DWT (Cortex-M) 通用定时器 示波器 + GPIO 循环计数 汇编指令分析 Cache miss分析 定位瓶颈 → 针对性优化

这张图的核心逻辑很简单:发现问题后,从三个维度同时下手。Profiling工具告诉你“哪个函数最慢”,时间测量告诉你“这段代码到底跑了多久”,代码审查则帮你找出“为什么这么慢”。三者结合,没有找不到的瓶颈。

3.5 实战建议

最后,给你几个我踩过坑之后总结的建议:

  • 先做“减法”:优化前,先删掉所有调试打印、冗余日志、不必要的断言。很多时候,删掉比优化更有效。
  • 关注“最差情况”:平均执行时间好看没用,要看最差情况下的执行时间。运动控制最怕“偶尔抖一下”。
  • 用GPIO翻脚做“穷人的示波器”:在关键代码段前后各放一个GPIO置位和清零,然后用示波器看脉宽。这个方法虽然土,但极其可靠,我至今还在用。

记住: 性能分析不是一次性的工作。每次修改代码后,都应该重新测量一遍。养成习惯,你会发现很多问题在早期就被扼杀在摇篮里了。


专注资料整理