sched discussion
[PATCH] sched/psi: use for_each_set_bit() in psi_group_change() task-count walk
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 跳过未设位"的手写模板,存在两段循环,每次都要走到当前最高置位为止。
补丁目标:
- 用内核通用宏
for_each_set_bit()替换手写循环。 - 借助编译时常量宽度命中
find_next_bit()的small_const_nbits()快路径。 - 让两段重复的循环结构变成统一、更易读的
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 自增 + 重新判断是否置位" 三件事分散在三行,意图不直观。
- 同样的逻辑在
clear和set两处重复出现,风格不统一。
新流程
+------------------------------------------------------+
| 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_SMP与NR_CPUS取不同值时的差异。 intvsunsigned long接口:Prateek 建议直接把形参改成unsigned long,省去本地变量;Usama 担心所有调用方传int且printk格式串要从%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,预计需要:
- 补充 benchmark 数据(至少一个 scheduler 压力场景 + 一个 memstall 场景)。
- 给出 asm 对比(x86_64 + arm64,参考 Usama 提供的 gist:https://gist.github.com/uarif1/e1bf78b54f50099b354b84684f880fda)。
- 视情况回退到
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 手工调优过的,必须用汇编 + 基准数据证明收益才能合入。