0/11 已展开

LLM 分析

sched/fair:让同步唤醒把 wakee 放到 waker 所在核的兄弟线程上

系列概况

  • 标题: [PATCH] sched/fair: Let sync wakeups target the waker's core
  • 作者: Madadi Vineeth Reddy <vineethr@linux.ibm.com>(IBM)
  • 版本: 单 patch(v1),讨论中引用了 Shubhang v3、Prateek 的备选实现
  • 规模: kernel/sched/fair.c49 +++++++++++++++/----------(38 行新增 / 11 行删除)
  • 修改文件: 1 个(kernel/sched/fair.c
  • 代码统计: 4 处函数签名扩展(select_idle_siblingselect_idle_coreselect_idle_cpu、前向声明)+ select_task_rq_fair() 中的同步判定
  • Message-ID: 20260801035532.260625-1-vineethr@linux.ibm.com
  • 完整性: patch 完整,包含 diff、commit message、性能数据(producer_consumer、hackbench、schbench);Signed-off-by: Madadi Vineeth Reddy <vineethr@linux.ibm.com>;x86 测试由 Kayra 提供 Tested-by

补丁目的

WF_SYNC 触发的同步唤醒路径能够识别“waker 即将让出 CPU 且其 runqueue 上只剩它自己”这种临界状态:在这种情况下把 waker 的 CPU 当作空闲看待,使 waker 所在核仍被算作 idle-core,wakee 就能落到这个核的兄弟 SMT 线程上。

这样做的两个收益:

  • 复用了 waker 核心刚刚留在 L1/L2 cache 里的热数据。
  • 避免把 wakee 串到 waker 还在跑的同一个硬件线程之后(no stacking),兄弟线程可以同时跑。

旧流程的问题

wake_affine_idle() 在 waker 的 runqueue 上只剩一个 runnable 任务时已经把 cpu(waker 的 CPU)作为目标返回;但紧跟着的 select_idle_sibling() 在做 idle-core 扫描时调用 available_idle_cpu(cpu),判定 waker 还在运行 ⇒ 忙,于是丢掉这个候选,继续扫 LLC 里的其他核。结果:

  • 如果 wakee 的 prev_cpu 闲且与 target 共享 cache,SIS 提前返回,无损失。
  • 一旦 prev_cpu 也忙,扫描无法分辨“这核真的忙”和“这核只跑着即将睡的 waker”,最终把 wakee 放到一个冷核上,浪费了 waker 留下的 cache 热数据。

新流程

select_task_rq_fair() 里把 WF_TTWU 的快速路径扩成:

if (wake_flags & WF_TTWU) {
    int sync_cpu = -1;
    if (want_affine && sync && new_cpu == cpu) {
        struct rq *rq = cpu_rq(cpu);
        if ((rq->nr_running - cfs_h_nr_delayed(rq)) == 1)
            sync_cpu = cpu;
    }
    return select_idle_sibling(p, prev_cpu, new_cpu, sync_cpu);
}

sync_cpu 一路下传到 select_idle_core()。在 idle-core 判定里:

bool sync_waker = (cpu == sync_cpu);
/* waker 即将 block 且没有别的 runnable,让它看起来 idle */
if (!available_idle_cpu(cpu) && !sync_waker) {
    idle = false;
    break;
}
/* 不把还在跑的 waker 当作 fallback */
if (!sync_waker && *idle_cpu == -1 && cpumask_test_cpu(cpu, cpus))
    *idle_cpu = cpu;

只有同时满足 (want_affine && sync && new_cpu == cpu) 且 waker rq 上只有一个 runnable 任务时,sync_cpu 才会被设为 waker 的 CPU;其它情况它都是 -1,行为完全等价于原代码,是 no-op。

  WF_SYNC wakeup, waker_rq has 1 task
  ----------------------------------
       waker           wakee
         |  blocks        ^
         v                |
   want_affine&sync? --yes--> sync_cpu = waker's cpu
         |
         v
   select_idle_sibling(..., sync_cpu)
         |
         v
   for each core in LLC:
       scan siblings of core
       if any sibling idle and core "looks idle":
           --> accept core as idle-core candidate
       where sync_waker makes waker look idle
         |
         v
   wakee -> sibling of waker's core (cache-hot, parallel)

关键实现

  • select_idle_core() 新增 sync_cpu 参数;对 sync_waker 既跳过“非 idle”判定,又不把它写进 *idle_cpu(因为它不是真空闲,只是马上要空),避免选出来当作 fallback 跑 wakee 自己。
  • select_idle_cpu()select_idle_sibling() 的签名同步扩展为转发 sync_cpuselect_task_rq_fair() 中的判定只对同步唤醒、且 waker 的 rq 仅有一个 runnable 任务的情况生效。
  • 触发条件足够保守:waker 的核心无空闲兄弟线程 → no-op;waker 的 rq 还有别的 runnable → no-op;prev_cpu 本来就是合法目标 → no-op;非 SMT 系统 → no-op。

类比

把 CPU 核心想成一间四人合住的宿舍,waker 刚把自己桌上的便签(cache)翻得一塌糊涂要去睡觉,wakee 是来接班的人。

  • 旧流程:舍管看到 waker 的床铺还有人(waker 还没走),就让 wakee 去隔壁空宿舍重新找桌子,便签全丢了。
  • 新流程:舍管注意到 waker 行李已经收好、马上空出床位(nr_running - cfs_h_nr_delayed == 1),就把 wakee 安排到同一宿舍的另一张床——既能接着用 waker 留下的便签(cache-hot),又不会两张床争一把椅子(避免同线程 stacking)。

也就是说:不要把“床还铺着”和“这人今晚要回家”混为一谈。

Highlight:风险与注意点

  1. wrap scan 顺序依赖:在 POWER11 SMT8 这种兄弟 CPU 紧邻排布上,for_each_cpu_wrap(cpu, cpus, target + 1) 第一个就扫到 waker 的兄弟,效果显著;但在 x86 这种兄弟在 cpu + nr_cores 的拓扑下,扫描会先撞到别的核,如果其中有真空闲核就被截走,收益被稀释甚至消失(Zhan Xusheng 提出此点)。补丁不回归,但收益可能高度依赖 SMT 编号方式。
  2. x86 Zen 3 SMT2 实测的两难:Kayra 在 Ryzen 7 5700X 上跑 perf bench sched pipe,cycles 下降 1.40%,但 cache-misses 反而上升 4.60%。这与“兄弟线程共享 cache”直觉冲突——可能是因为 SMT2 下 waker 兄弟就是 prev_cpu 或 target 之外的真冷路,或是 sibling 上 wakee 与 waker 抢 LLC 容量,提示后续需要拆分 L1/L2/L3 misses 验证。
  3. API 设计的多种形态:Prateek 提议把改动上提到 select_idle_sibling() 的“!has_idle_core && cpus_share_cache(prev, target)”分支里直接初始化 idle_cpu;Chen Yu 提议抽出一个 select_idle_sync_core() 辅助函数(sync_corebool)。两条思路都比当前实现更“早期决策”,但都需要补 prev_cpu 拓扑比较,主线尚未合拢。
  4. 保守触发条件:仅在 want_affine && sync && new_cpu == cpu 且 waker rq 唯一可运行时才设 sync_cpu,已排除大多数误判场景;但 (rq->nr_running - cfs_h_nr_delayed(rq)) == 1 这个等式本身是否包含 cfs_h_nr_delayed 的延迟唤醒影响,需要再读一遍 delayed accounting 才完全放心。
  5. 与并行讨论的关联:本补丁明确说是“disjoint case”——Shubhang v3 走的是 wake_affine 直接返回 waker CPU(被 v3 限制在 !sched_smt_active()),Prateek 之前的方案是 “无 idle-core 时把 wakee 堆到 waker 的 rq 上”。本补丁负责“同 LLC 存在 idle-core 时把 wakee 放到 waker 核的兄弟线程”,三者互补而不是替换。

一句话总结

通过把“waker 即将让出且 rq 仅剩自己”的 waker CPU 临时视为空闲,让 select_idle_core() 在 SMT 上保留 waker 核作为 idle-core 候选,从而把同步唤醒的 wakee 放到 waker 核的兄弟线程上,兼顾 cache 复用与并行执行。