9、性能优化:SMP性能调优、热点分析、锁竞争优化、核间延迟测量

好,咱们进入SMP实战中最有挑战性的一环——性能优化。说实话,很多工程师把SMP跑起来就觉得完事了,其实真正的硬仗才刚刚开始。你想想看,两个核干活,如果配合不好,效率可能还不如单核。我在项目中就见过这样的案例:加了第二个核,总吞吐量只提升了20%,你说气不气人?

9.1 性能调优:从数据说话开始

我个人习惯,拿到一个SMP系统,第一件事不是看代码,而是先搭一套性能基准测试。没有基线数据,你根本不知道优化有没有效果。

常用的性能指标有这么几个:

  • 系统吞吐量:单位时间内完成的任务数。比如网络报文处理量、传感器数据采集量。
  • 响应延迟:从任务就绪到开始执行的时间。SMP下这个指标特别容易恶化。
  • CPU利用率均衡度:两个核的负载差距。理想情况是50%对50%,但现实中经常是80%对20%。
  • 上下文切换频率:切换太频繁说明调度策略有问题。

核心观点:性能调优不是拍脑袋,而是基于数据的迭代过程。每次改一个变量,重新跑基准,对比数据,再决定下一步。

我在RT-Thread上常用的调优工具是rt_system_scheduler_startrt_thread_control接口,配合自定义的统计钩子函数。比如这样:

/* 注册调度钩子,统计每个核的运行时间 */
static rt_uint64_t cpu_runtime[RT_CPUS_NR];

void scheduler_hook(struct rt_thread *from, struct rt_thread *to)
{
    int cpu = rt_hw_cpu_id();
    cpu_runtime[cpu] += rt_tick_get() - last_tick[cpu];
    last_tick[cpu] = rt_tick_get();
}

void init_perf_monitor(void)
{
    rt_scheduler_sethook(scheduler_hook);
}

嗯,这里要注意:钩子函数里不能做太重的操作,否则会影响调度本身的性能。我刚开始就犯过这个错,在钩子里打印日志,结果系统直接卡死。

9.2 热点分析:找到真正的瓶颈

热点分析说白了就是找「谁在偷懒,谁在忙死」。SMP系统里,热点通常出现在三个地方:

  1. 全局数据结构:比如任务就绪队列、内存池。所有核都要访问,竞争激烈。
  2. 中断处理:中断来了,所有核都可能响应,但最终只有一个核处理。
  3. 内核对象操作:信号量、互斥量、消息队列的创建和删除。

我建议用两种方法做热点分析:

  • 静态分析:看代码中哪些全局变量被频繁加锁。用grep搜一下rt_enter_criticalrt_hw_spin_lock,数数调用次数。
  • 动态分析:在关键函数入口和出口插入时间戳,计算执行时间。比如用rt_tick_get()或者硬件周期计数器。

小技巧:我曾经在调试一个网络协议栈时,发现90%的CPU时间花在rt_memcpy上。你以为瓶颈在锁?其实在数据拷贝。优化方向完全不一样。

热点分析的结果,我习惯用表格记录下来:

热点函数 调用频率(次/秒) 平均执行时间(us) 总CPU占比
rt_schedule 12000 3.2 38.4%
rt_sem_take 8500 1.8 15.3%
rt_mp_alloc 3200 4.5 14.4%

看到这个表,你就知道该优化谁了。调度器占了38%,那肯定得先动它。

9.3 锁竞争优化:别让锁成为瓶颈

锁竞争是SMP性能的头号杀手。两个核抢同一把锁,一个在等,一个在忙,白白浪费CPU周期。我见过最夸张的情况,一个系统里80%的时间花在自旋锁等待上。

优化锁竞争,我总结了几个实战经验:

  • 减小锁粒度:把大锁拆成小锁。比如全局任务队列,可以拆成每个优先级一个队列,每个队列一把锁。
  • 读写锁分离:读操作远多于写操作时,用读写锁代替互斥锁。RT-Thread的rt_rwlock就是干这个的。
  • 无锁数据结构:某些场景可以用原子操作代替锁。比如引用计数、标志位。
  • 锁的层级化:规定锁的获取顺序,避免死锁,也减少等待时间。

注意:锁优化不是越细越好。锁太细,获取释放的开销反而变大。我曾经把一个全局锁拆成16把细锁,结果性能反而下降了5%。后来改成8把,才达到最优。

代码层面,我常用的优化手法是这样的:

/* 优化前:全局一把大锁 */
static struct rt_spinlock big_lock;
void process_packet(struct packet *pkt)
{
    rt_spin_lock(&big_lock);
    /* 处理所有包 */
    rt_spin_unlock(&big_lock);
}

/* 优化后:按CPU核心分锁 */
static struct rt_spinlock per_cpu_lock[RT_CPUS_NR];
void process_packet(struct packet *pkt)
{
    int cpu = rt_hw_cpu_id();
    rt_spin_lock(&per_cpu_lock[cpu]);
    /* 只处理本核的包 */
    rt_spin_unlock(&per_cpu_lock[cpu]);
}

你看,这个改动很简单,但效果立竿见影。每个核只抢自己的锁,完全没有竞争。

9.4 核间延迟测量:精确到微秒

核间延迟,说白了就是一个核发消息给另一个核,对方多久能收到。这个指标直接影响SMP系统的实时性。

测量方法我推荐两种:

  • 软件方法:用rt_smp_ipi_send发送核间中断,在接收端记录时间戳。精度取决于系统时钟,一般能到微秒级。
  • 硬件方法:用GPIO引脚输出脉冲,示波器直接量。精度最高,但需要额外硬件。

我习惯在RT-Thread里这样测:

/* 发送端 */
void measure_latency_send(void)
{
    volatile uint64_t *ts = &shared_timestamp;
    *ts = read_cycle_counter();  /* 读硬件周期计数器 */
    rt_smp_ipi_send(1, IPI_LATENCY_TEST);  /* 发往核1 */
}

/* 接收端(在核1的中断处理中) */
void ipi_handler(int irq, void *param)
{
    uint64_t recv_time = read_cycle_counter();
    uint64_t send_time = shared_timestamp;
    uint64_t latency = recv_time - send_time;
    /* latency就是核间延迟,单位是周期数 */
}

经验数据:在我调试过的Cortex-A7双核平台上,核间延迟大约在0.5~2微秒之间。如果超过5微秒,说明系统负载太高或者中断响应有问题。

影响核间延迟的因素主要有:

  • Cache一致性协议:核间通信涉及Cache同步,这个时间不可忽略。
  • 中断优先级:如果接收核正在处理高优先级中断,IPI会被延迟。
  • 系统负载:核越忙,响应IPI越慢。

我曾经在一个项目中,发现核间延迟忽高忽低,从1微秒跳到10微秒。排查了半天,原来是另一个核在跑一个死循环任务,把中断关了。嗯,这种坑踩过一次就记住了。

好了,性能优化这块内容不少,但核心就三句话:用数据说话、锁要精细、延迟要量化。你把这些方法用到实际项目中,SMP的性能至少能提升30%以上。下一章咱们聊聊多核调试,那又是另一番天地了。