3. 性能指标与评估方法
在 RT-Thread 的性能调试与优化中,第一步不是“改代码”,而是“把问题量化”。只有把系统行为转化为可重复、可对比的数值指标,优化才有方向,验证才有依据。本节围绕五个最常用的关键指标展开:CPU 占用率、内存占用、中断延迟、线程切换时间和吞吐量,分别说明它们的定义、影响因素、测量方法与典型误区。
一、五个关键指标的定义与关注点
RT-Thread 作为实时操作系统,性能评估通常同时关注“实时性”和“资源效率”。实时性由中断延迟、线程切换时间、调度抖动体现;资源效率由 CPU 占用率、内存占用体现;系统处理能力则由吞吐量体现。它们之间往往存在权衡:提高吞吐量可能增加延迟,降低内存占用可能牺牲缓存或缓冲能力。
| 指标 | 定义 | 主要影响因素 | 常用测量手段 |
|---|---|---|---|
| CPU 占用率 | 单位时间内 CPU 执行有效任务或中断处理的时间占比 | 任务周期、优先级、阻塞/唤醒频率、中断频率 | 空闲线程钩子统计、RT-Thread Studio 查看、GPIO 翻转+示波器 |
| 内存占用 | 静态占用(代码/数据/BSS)与动态占用(堆、栈、内存池)之和 | 线程栈大小、动态分配频率、内存池碎片 | list_mem、list_thread、memtrace、map 文件分析 |
| 中断延迟 | 从中断请求发生到中断服务程序第一条指令执行的时间 | 中断嵌套、关中断时长、中断优先级、Cache 命中 | GPIO 翻转+示波器、DWT 周期计数、逻辑分析仪 |
| 线程切换时间 | 从当前线程让出 CPU 到目标线程开始执行的时间 | 调度器实现、就绪表查找、上下文保存量、FPU 使用 | GPIO 翻转、DWT 计时、rt_tick 差值粗测 |
| 吞吐量 | 单位时间内系统成功处理的数据量或事务数 | 任务划分、IPC 效率、缓冲区大小、CPU 主频 | 计数器统计、串口/网络压测、perf 类工具 |
二、CPU 占用率与内存占用的测量方法
CPU 占用率最常用的方法是“空闲线程钩子统计法”。RT-Thread 在空闲线程中挂接钩子,每次进入空闲线程累加计数,再结合系统 tick 换算出空闲占比,从而得到 CPU 占用率。这种方法开销小、无需额外硬件,适合长期在线监测。
/* 空闲线程钩子统计 CPU 占用率示例 */
static rt_uint32_t idle_cnt = 0;
static rt_tick_t last_tick = 0;
static void idle_hook(void)
{
idle_cnt++;
}
static void cpu_usage_sample(void)
{
rt_tick_t now = rt_tick_get();
rt_tick_t delta = now - last_tick;
rt_uint32_t idle = idle_cnt;
idle_cnt = 0;
last_tick = now;
/* 近似:空闲计数 / 总 tick 数,得到空闲率 */
rt_kprintf("idle rate: %d%%\n", idle * 100 / delta);
}
/* 初始化时挂接 */
rt_thread_idle_sethook(idle_hook);
内存占用则分为静态和动态两部分。静态占用可通过编译生成的 map 文件查看代码段、数据段和 BSS 段大小;动态占用可通过 FinSH 命令实时查看。需要特别关注线程栈的使用峰值,因为栈溢出往往表现为“随机死机”,而不是直接报错。
msh /> free
total memory: 32768
used memory : 12288
maximum allocated memory: 16384
msh /> list_thread
thread pri status sp stack size max used left tick error
-------- --- -------- ------ ---------- -------- ---------- -----
main 10 ready 0x0000 0x00000400 0x00000120 0x0000000a 000
tidle 31 ready 0x0000 0x00000200 0x00000080 0x00000005 000
其中 max used 表示线程栈历史最大使用量,是判断栈是否足够的核心依据。若 max used 接近 stack size,应适当增大栈或检查是否存在大局部变量、深递归。
三、中断延迟与线程切换时间的测量
中断延迟和线程切换时间都属于“微秒级”指标,普通 rt_tick 只有毫秒级分辨率,无法直接测量。推荐使用 GPIO 翻转配合示波器或逻辑分析仪:在中断入口翻转一个 GPIO,在中断源触发处翻转另一个 GPIO,两者时间差即为中断延迟。若没有示波器,也可使用 Cortex-M 的 DWT 周期计数器进行粗测。
/* DWT 周期计数测量线程切换时间示例 */
__STATIC_INLINE void dwt_init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
rt_uint32_t t0 = DWT->CYCCNT;
rt_thread_yield(); /* 主动触发一次线程切换 */
rt_uint32_t t1 = DWT->CYCCNT;
rt_kprintf("switch cycles: %u\n", t1 - t0);
测量时要注意:DWT 计数受主频和 Cache 影响,结果应多次采样取平均值或最大值;同时要区分“主动让出”和“被抢占”两种切换路径,它们的耗时并不相同。
四、吞吐量的评估与场景案例
吞吐量通常用“单位时间处理的消息数/字节数”表示。以串口数据采集为例:线程 A 从串口读取数据,通过消息队列发送给线程 B,线程 B 解析并写入文件系统。若消息队列深度不足,线程 A 会频繁阻塞,吞吐量下降;若线程 B 优先级过低,消息积压也会导致丢包。
/* 吞吐量统计示例:统计每秒处理的消息数 */
static rt_uint32_t msg_cnt = 0;
static void consumer_thread_entry(void *param)
{
while (1)
{
if (rt_mq_recv(&mq, &msg, sizeof(msg), RT_WAITING_FOREVER) == RT_EOK)
{
msg_cnt++;
/* 处理消息 */
}
}
}
static void throughput_report(void)
{
rt_kprintf("throughput: %u msg/s\n", msg_cnt);
msg_cnt = 0;
}
五、常见误区与复盘建议
- 只看平均值,不看最坏值:实时系统更关心最大中断延迟和最大切换时间,平均值会掩盖偶发卡顿。
- 忽略测量本身的开销:频繁打印日志、GPIO 翻转过多都会干扰被测系统,应尽量使用轻量计数。
- 混淆 tick 与真实时间:rt_tick 精度通常为 1ms,不能用于微秒级指标测量。
- 脱离负载谈指标:空载下的 CPU 占用率没有参考价值,必须给出任务数量、周期和中断频率。
- 只测一次就下结论:应多次采样,记录最小值、最大值和分布,才能判断系统是否稳定。
综上,CPU 占用率、内存占用、中断延迟、线程切换时间和吞吐量共同构成了 RT-Thread 性能评估的基础指标体系。测量时务必明确负载条件、统计口径和采样方式,并优先关注最坏值而非平均值。只有把指标测准、测稳,后续的优化工作才能真正做到有的放矢。