9、性能优化:SMP性能调优、热点分析、锁竞争优化、核间延迟测量
好,咱们进入SMP实战中最有挑战性的一环——性能优化。说实话,很多工程师把SMP跑起来就觉得完事了,其实真正的硬仗才刚刚开始。你想想看,两个核干活,如果配合不好,效率可能还不如单核。我在项目中就见过这样的案例:加了第二个核,总吞吐量只提升了20%,你说气不气人?
9.1 性能调优:从数据说话开始
我个人习惯,拿到一个SMP系统,第一件事不是看代码,而是先搭一套性能基准测试。没有基线数据,你根本不知道优化有没有效果。
常用的性能指标有这么几个:
- 系统吞吐量:单位时间内完成的任务数。比如网络报文处理量、传感器数据采集量。
- 响应延迟:从任务就绪到开始执行的时间。SMP下这个指标特别容易恶化。
- CPU利用率均衡度:两个核的负载差距。理想情况是50%对50%,但现实中经常是80%对20%。
- 上下文切换频率:切换太频繁说明调度策略有问题。
核心观点:性能调优不是拍脑袋,而是基于数据的迭代过程。每次改一个变量,重新跑基准,对比数据,再决定下一步。
我在RT-Thread上常用的调优工具是rt_system_scheduler_start和rt_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系统里,热点通常出现在三个地方:
- 全局数据结构:比如任务就绪队列、内存池。所有核都要访问,竞争激烈。
- 中断处理:中断来了,所有核都可能响应,但最终只有一个核处理。
- 内核对象操作:信号量、互斥量、消息队列的创建和删除。
我建议用两种方法做热点分析:
- 静态分析:看代码中哪些全局变量被频繁加锁。用
grep搜一下rt_enter_critical和rt_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%以上。下一章咱们聊聊多核调试,那又是另一番天地了。