0/6 已展开

LLM 分析

sched/psi:在 psi_group_change() 的任务计数遍历中使用 for_each_set_bit()

系列概况

  • 标题:[PATCH] sched/psi: use for_each_set_bit() in psi_group_change() task-count walk
  • 作者:Usama Arif usama.arif@linux.dev
  • 版本:v1,单 patch
  • 规模:1 个文件 kernel/sched/psi.c,7 处插入 / 7 处删除
  • 修改文件kernel/sched/psi.c,函数 psi_group_change()
  • 代码统计:净增 0 行,纯等行替换
  • Message-ID:6 封邮件,主 patch ID 20260714142057.181135-1-usama.arif@linux.dev
  • 完整性:thread 完整,主 patch + Prateek Nayak 的 Reviewed-by + psi 维护者 Johannes Weiner 的反对/谨慎意见

补丁目的

psi_group_change() 每次任务切换或任务状态变化都会被调用,对每个祖先 psi_group 各执行一次 clear / set 位掩码的遍历,递减或递增 groupc->tasks[t]。位掩码宽度上限是 NR_PSI_TASK_COUNTS = 4,但原代码用"循环 + 清位 + continue 跳过未设位"的手写模板,存在两段循环,每次都要走到当前最高置位为止。

补丁目标:

  1. 用内核通用宏 for_each_set_bit() 替换手写循环。
  2. 借助编译时常量宽度命中 find_next_bit()small_const_nbits() 快路径。
  3. 让两段重复的循环结构变成统一、更易读的 for_each_set_bit 写法。

旧流程的问题

+------------------------------------------------------+
| old loop A (handwritten bit scan over `clear`)       |
|                                                      |
|   for (t = 0, m = clear; m; m &= ~(1UL << t), t++) { |
|       if (!(m & (1UL << t)))                         |
|           continue;                                  |
|      <decrement groupc->tasks[t]>                   |
|   }                                                  |
|                                                      |
| old loop B (handwritten bit scan over `set`)         |
|                                                      |
|   for (t = 0; set; set &= ~(1UL << t), t++)          |
|       if (set & (1UL << t))                          |
|           groupc->tasks[t]++;                        |
+------------------------------------------------------+

问题:

  • 当只有 bit 3 被置位时,循环仍会迭代 4 次,逐个测试 m & (1UL << t),再被 continue 跳过。
  • "清位 + t 自增 + 重新判断是否置位" 三件事分散在三行,意图不直观。
  • 同样的逻辑在 clearset 两处重复出现,风格不统一。

新流程

+------------------------------------------------------+
| new loop A: for_each_set_bit over `clear_bits`       |
|                                                      |
|   clear_bits = clear;                                |
|   for_each_set_bit(t, &clear_bits,                    |
|                    NR_PSI_TASK_COUNTS) {             |
|       <decrement groupc->tasks[t]>                   |
|   }                                                  |
|                                                      |
| new loop B: for_each_set_bit over `set_bits`         |
|                                                      |
|   set_bits = set;                                    |
|   for_each_set_bit(t, &set_bits,                      |
|                    NR_PSI_TASK_COUNTS)               |
|       groupc->tasks[t]++;                            |
+------------------------------------------------------+

for_each_set_bit()nbits 为编译期常量且不大于 BITS_PER_LONG 时,会通过 find_next_bit()small_const_nbits() 快路径:一次 word 加载 + GENMASK(nbits-1, 0) + __ffs(word),在支持的架构上还会进一步退化为 TZCNT/BSF(x86)或 RBIT+CLZ(arm64)。

Patch 概览

  • 把局部变量从 unsigned int t, m; 改成 unsigned long clear_bits, set_bits; unsigned int t;,保留 t 作为位索引。
  • clear 循环用 for_each_set_bit + NR_PSI_TASK_COUNTS 替换,并去掉 if (!(m & (1 << t))) continue;
  • set 循环同样替换为 for_each_set_bit 一行调用。
  • commit message 明确写 "No functional change intended",无行为变化,仅清理 + 微优化。

关键实现

static void psi_group_change(struct psi_group *group, int cpu,
                             u64 now, bool wake_clock)
{
    struct psi_group_cpu *groupc;
    unsigned long clear_bits, set_bits;
    unsigned int t;
    u32 state_mask;

    lockdep_assert_rq_held(cpu_rq(cpu));
    ...
    /* walk `clear`: decrement counters for bits being cleared */
    clear_bits = clear;
    for_each_set_bit(t, &clear_bits, NR_PSI_TASK_COUNTS) {
        if (groupc->tasks[t]) {
            groupc->tasks[t]--;
        } else if (!psi_bug) {
            /* fire psi_bug */
        }
    }

    /* walk `set`: increment counters for bits being set */
    set_bits = set;
    for_each_set_bit(t, &set_bits, NR_PSI_TASK_COUNTS)
        groupc->tasks[t]++;
    ...
}

NR_PSI_TASK_COUNTS 为 4(oncpu / memstall / iowstall / some),宏内部走 small_const_nbits(4):一次 word 加载 + GENMASK(3,0) + __ffs(word),最终落到一条位扫描指令。

调用方关系:

psi_task_switch()            psi_task_change()
        | |
        +-----------+---------------+
 |
                    v
        psi_group_change(group, cpu, now, wake_clock)
                    |
                    +---> iterate over each ancestor psi_group
                    |
                    +---> per group: scan clear bits (decrement)
                    +---> per group: scan set bits   (increment)

类比

把 4 个格子想象成办公桌上的 4 个文件筐,每个筐贴了一张"忙/闲"标签。psi_group_change() 每收到一次任务切换通知,就要核对哪些标签从"忙"翻成"闲"(clear),哪些从"闲"翻成"忙"(set)。

  • 旧做法:你站在桌前,从左到右逐个拿起标签看一眼(哪怕这个筐今天根本没动),再决定要不要翻面记录。一次通知最多检查 4 次。
  • 新做法:用一只手电筒照标签,只在确实亮着的标签(置位的位)停下来登记,其他没亮的直接跳过。一次通知平均只看 1 次甚至 0 次。手电筒就是 for_each_set_bit(),它自带"只照亮置位"的镜头。

Highlight:风险与注意点

  • 历史性能敏感路径:Johannes Weiner 提醒,这段代码最初用的是 ffs,后来是为了 scheduler benchmark 成绩被手工调过的。直接换成通用宏不一定是性能改进,需要做 asm 对比 + 基准测试。
  • gcc 可能产生更多代码:Weiner 实测后指出,新版在他的编译器上生成的指令数更多。需要在多种架构与 CONFIG_* 组合下核对,特别是 CONFIG_SMPNR_CPUS 取不同值时的差异。
  • int vs unsigned long 接口:Prateek 建议直接把形参改成 unsigned long,省去本地变量;Usama 担心所有调用方传 intprintk 格式串要从 %x 变成 %lx,并相信赋值本身应被优化掉。这是风格取舍,不影响功能。
  • 没有功能变化:作者明确声明 "No functional change intended"。审阅者应聚焦于生成的汇编与基准,而非行为差异。
  • 2018 年旧讨论:Weiner 引用了 2018 年的旧 thread 作为先例(https://lore.kernel.org/all/20180718120318.GC2476@hirez.programming.kicks-ass.net/),说明当时就已经走过类似的"用通用宏 vs 手工 ffs"权衡。

版本变化

本 thread 只有 v1,没有 v2/v3 出现,因此没有可比较的版本差异。如果要出 v2,预计需要:

  1. 补充 benchmark 数据(至少一个 scheduler 压力场景 + 一个 memstall 场景)。
  2. 给出 asm 对比(x86_64 + arm64,参考 Usama 提供的 gist:https://gist.github.com/uarif1/e1bf78b54f50099b354b84684f880fda)。
  3. 视情况回退到 unsigned long clear_bits 局部变量写法,或按 Prateek 建议改形参类型。

与其他相关 patch 系列的关联

  • 这是针对 PSI(Pressure Stall Information)子系统的小型清理。PSI 同期还有 psi: fix wrong PSI statistics after CPU hotplug 之类的修复 patch,本系列与之正交,只是微观优化。
  • 与 scheduler 的 task count 维护相关代码都从 psi_task_switch() / psi_task_change() 进入 psi_group_change(),本 patch 会让这两个热路径的位扫描行为发生微小变化,需结合 scheduler benchmark 综合评估。

一句话总结

一个针对 psi_group_change() 的 7/7 微清理:把手写"清位 + 跳过未设位"循环换成 for_each_set_bit(),号称借 small_const_nbits() 快路径更快、更易读;但 psi 维护者提醒该路径历史上是为 scheduler benchmark 手工调优过的,必须用汇编 + 基准数据证明收益才能合入。