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;
}
测量吞吐量时,应同时记录 CPU 占用率和最大延迟。高吞吐量若以 90% 以上 CPU 占用率为代价,系统将失去实时响应能力,这种“高吞吐”在嵌入式场景中往往不可接受。

五、常见误区与复盘建议

  • 只看平均值,不看最坏值:实时系统更关心最大中断延迟和最大切换时间,平均值会掩盖偶发卡顿。
  • 忽略测量本身的开销:频繁打印日志、GPIO 翻转过多都会干扰被测系统,应尽量使用轻量计数。
  • 混淆 tick 与真实时间:rt_tick 精度通常为 1ms,不能用于微秒级指标测量。
  • 脱离负载谈指标:空载下的 CPU 占用率没有参考价值,必须给出任务数量、周期和中断频率。
  • 只测一次就下结论:应多次采样,记录最小值、最大值和分布,才能判断系统是否稳定。
优化前先建立基线:在固定硬件、固定负载、固定编译选项下记录一组指标,后续任何改动都与基线对比。没有基线的优化,很容易变成“感觉变快了”。
RT-Thread 关键性能指标与测量路径 RT-Thread 系统 任务 / 中断 / IPC / 内存 CPU 占用率 空闲钩子 / 采样 内存占用 free / list_thread 中断延迟 GPIO / DWT 线程切换时间 DWT / 翻转 吞吐量 计数器 / 压测 测量工具与输出 软件统计 FinSH / 钩子 / 计数器 硬件测量 示波器 / 逻辑分析仪 周期计数 DWT / CYCCNT 结果输出 基线 / 对比 / 报告

综上,CPU 占用率、内存占用、中断延迟、线程切换时间和吞吐量共同构成了 RT-Thread 性能评估的基础指标体系。测量时务必明确负载条件、统计口径和采样方式,并优先关注最坏值而非平均值。只有把指标测准、测稳,后续的优化工作才能真正做到有的放矢。