sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus

CFS 调度器 · cache-aware 调度 · task_cache_work 扫描开销优化

💡 一句话总结

cache-aware 调度(CONFIG_SCHED_CACHE)会周期性地为每个进程寻找「最偏爱的 LLC(最后一级缓存域)」,靠的是 task_cache_work() 每隔 llc_epoch_period(默认 10ms)扫描全系统所有 CPU 来计算各缓存域的占用热度。在多 NUMA 大机器上(如 384 CPU)这种「全量扫描」绝大部分落在从未跑过该进程、或早已超时的 CPU 上,造成显著开销。本补丁给每个进程的 mm_struct 加一个 visited_cpus 位图,只记录「该进程真正运行过、且近期仍在运行的 CPU」,task_cache_work() 只在这张位图上扫描,并淘汰超过 llc_epoch_affinity_timeout(5 个 epoch,约 50ms)未访问的 CPU。作者自报:Redis(valkey-benchmark)场景扫描 CPU 数从 384 降到 14~16,task_cache_work 在 perf 中的周期占比从 0.81% 降到 0.02%,p99 延迟从「相对 baseline 恶化 -25.68%」回收到「仅 -1.14%」。属于优化补丁(performance),状态 In Review。

📋 补丁基本信息

项目内容
补丁类型优化(performance)——降低 cache-aware 调度热路径 task_cache_work() 的扫描开销
状态In Review(v9,2026-07-31 提交,尚未合入主线)
当前版本v9 · v9 补丁链接
版本演进 v1(04-15 前)→ v2(04-14)→ v3(04-23)→ v4(06-18)→ v5(07-09)→ v6(07-17)→ v7(07-20)→ v8(07-23,+46/-56)→ v9(07-31,当前版,+47/-57)
(各版本链接来自 v9 cover letter 的 Changes history,均为 lore 官方 URL;主分析对象为任务指定 message-id 对应的 v6 补丁 1/2)
作者机构Luo Gengkun(luogengkun2@huawei.com,华为)
提交日期2026-07-17(v6)~ 2026-07-31(v9)
改动范围include/linux/mm_types.h + include/linux/sched.h + kernel/sched/fair.c,+46/-56 行(v8),3 文件;当前版 v9 为 +47/-57
核心函数task_cache_work() / fraction_mm_sched() / account_mm_sched()
原始链接lore Message-ID(v6 补丁 1/2)

📊 速览卡片

核心机制
visited_cpus 位图
优化目标
减扫描 CPU 数
适用场景
多 NUMA 大机器
实测提升
384→16 CPU

🎯 解决什么问题

背景 / 原始动机
cache-aware 调度(CONFIG_SCHED_CACHE)由 Intel Tim Chen / Chen Yu 系列补丁引入主线(基础设施 commit df0d98475954,2026-04-01;特性合并 a26d9208c137,2026-05-19)。它让进程「偏爱」某个 LLC 缓存域:把进程的线程尽量聚到同一个 LLC 里,减少跨缓存域访问的 miss。作者在 v9 cover letter 中描述问题原文:"The overhead of task_cache_work() is high, especially in multi-NUMA systems. Currently, task_cache_work() tries to find the pref_llc by scanning all CPUs in the system. However, most of these scans are meaningless, such as those for CPUs that have never been visited or were accessed a long time ago."(多 NUMA 系统下 task_cache_work() 开销很高,它扫描全系统所有 CPU 来找偏爱 LLC,但大部分扫描毫无意义——那些 CPU 从未被访问或早已超时)。
系统层面:全量扫描 × 每 10ms 一次 = 周期性高开销
task_cache_work() 是每进程的延迟工作(task_work),由 task_tick_cache() 在每 tick 触发,但通过 next_scan 门控确保每 llc_epoch_period(默认 10ms)只执行一次。它的目标是:遍历每个 CPU 的 LLC 调度域 sched_domain_span(sd),对域内每个 rq 调用 fraction_mm_sched()(在 cpu_epoch_lock 下取锁 + 更新几何级数占用率),累加出该 LLC 域的总占用 a_occ,最终把「占用最高的 LLC 域」选为进程新的 pref_llc(mm->sc_stat.cpu)。问题在于:它在每个 epoch 都对全系统所有 CPU 做一次取锁 + 算术。在 384 CPU 的多 NUMA 服务器上,每次扫描 384 个 rq,其中绝大多数 rq 上该进程从未运行过(占用率恒为 0),属于纯浪费。
场景层面:多实例 Redis / valkey 多 NUMA 部署
  • 负载特征:多实例部署(multi-instance)场景下,每个 redis-server / valkey-server 进程只跑在一小撮 CPU 上,但它的 task_cache_work() 每 10ms 就要把全系统几百个 CPU 全部扫一遍。
  • 硬件配置:AMD 多路 / 多 CCD 服务器,NUMA 节点多、CPU 总数大(trace 显示 scan=384),跨缓存域访问成本高,正是 cache-aware 调度想优化的对象。
  • 因果:cache-aware 调度本身在 Redis 上把 p99 从 0.436ms 恶化到 0.554ms(-25.68%,作者自报)——恶化主因正是 task_cache_work() 的周期性全量扫描(perf 中占 0.81% 周期)。这个「防缓存 miss」的机制因为自身扫描开销反而拖累了延迟敏感型负载。
受影响负载:多 NUMA 大机上、进程 CPU 足迹远小于全系统 CPU 数的多实例延迟敏感负载(Redis/valkey、数据库实例等) · 因果:扫描范围 = 全系统 CPU 数,而真实需要的只是「该进程访问过的 CPU」

🧩 核心机制

核心思路一句话:把「全系统所有 CPU」的扫描范围,收敛成「该进程最近真正运行过的 CPU」。实现上引入 per-mm 的 visited_cpus 位图 + per-CPU 的 epoch_last_visit 时间戳,并在 fraction_mm_sched() 中做超时淘汰。

从系统层面看
三个函数配合完成「记录访问 → 淘汰超时 → 只扫访问过的 CPU」:
  • 记访问(account_mm_sched()):进程在某 CPU 的 rq 上运行时,会做运行时记账。补丁在此处更新 pcpu_sched->epoch_last_visit = epoch 并 cpumask_set_cpu(cpu, visited_cpus) —— 表示「这个 CPU 最近被该进程用过」。
  • 淘汰超时(fraction_mm_sched()):扫描时先 __update_mm_sched(),再判断 (rq->cpu_epoch - epoch_last_visit) > llc_epoch_affinity_timeout(超 5 个 epoch 未访问)就把该 CPU 从 visited_cpus 清掉并返回 0 —— 既淘汰了过期 CPU,又让返回值天然「无贡献」。
  • 只扫访问过(task_cache_work()):外层先 cpumask_and(cpus, cpu_online_mask, visited_cpus),内层 for_each_cpu_and(i, sched_domain_span(sd), cpus) —— 遍历每个 LLC 域时只处理「同时位于该域且被访问过」的 CPU。
由于一个进程实际跑过的 CPU 数远小于全系统 CPU 数(trace 显示 384 → 14~16),每次扫描的取锁与算术次数下降一个数量级,这就是收益来源。v9 额外把内层循环从直接用 mm->sc_stat.visited_cpus 改为用本地 cpus 副本,缩短读取 pcpu_sched 的竞态窗口。
task_cache_work 扫描范围对比:全部 CPU vs 仅访问过的 CPU
图 1:改前全量扫描 384 个 CPU vs 改后只扫 visited_cpus(约 14~16 个)。
来源:基于 lore v6/v9 补丁 diff 与 cover letter trace 数据绘制
步骤操作目的
记访问account_mm_sched() 中更新 epoch_last_visit + cpumask_set_cpu()标记「该进程最近在此 CPU 运行过」
淘汰超时fraction_mm_sched() 中 cpu_epoch - epoch_last_visit > timeout → 清位图 + 返回 0移走过期 CPU,避免长期无效扫描
只扫访问过task_cache_work() 中 cpumask_and(cpus, online, visited_cpus) + for_each_cpu_and()把扫描集合收敛到真实访问过的 CPU
生命周期mm_alloc_sched_noprof() 分配 / mm_destroy_sched() 释放位图位图随进程生命周期分配/回收

🔬 关键代码

以下 diff 逐字取自 lore v9 补丁 1/2(20260731024417.1106503-2,当前版;v6 的核心逻辑与之相同,v8 起删除 get_scan_cpumasks())。

diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index b18c2b2e7d2c..35559079e4d4 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -1620,6 +1620,11 @@ static inline int mm_alloc_sched_noprof(struct mm_struct *mm)
 	if (!pcpu_sched)
 		return -ENOMEM;
 
+	if (!zalloc_cpumask_var(&mm->sc_stat.visited_cpus, GFP_KERNEL)) {
+		free_percpu(pcpu_sched);
+		return -ENOMEM;
+	}
+
 	mm_init_sched(mm, pcpu_sched);
 	return 0;
 }
@@ -1630,6 +1635,7 @@ static inline void mm_destroy_sched(struct mm_struct *mm)
 {
 	free_percpu(mm->sc_stat.pcpu_sched);
 	mm->sc_stat.pcpu_sched = NULL;
+	free_cpumask_var(mm->sc_stat.visited_cpus);
 }
 #else /* !CONFIG_SCHED_CACHE */
 
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 373bcc0598d1..b461a71a65da 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -2388,6 +2388,7 @@ static __always_inline int task_mm_cid(struct task_struct *t)
 struct sched_cache_time {
 	u64 runtime;
 	unsigned long epoch;
+	unsigned long epoch_last_visit;
 };
 
 struct sched_cache_stat {
@@ -2398,6 +2399,7 @@ struct sched_cache_stat {
 	unsigned long next_scan;
 	unsigned long footprint;
 	int cpu;
+	cpumask_var_t visited_cpus;
 } ____cacheline_aligned_in_smp;
 
 #else
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index d78467ec6ee1..2bb370b38b79 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -1585,6 +1585,7 @@ void mm_init_sched(struct mm_struct *mm,
 		pcpu_sched->runtime = 0;
 		/* a slightly stale cpu epoch is acceptible */
 		pcpu_sched->epoch = rq->cpu_epoch;
+		pcpu_sched->epoch_last_visit = rq->cpu_epoch;
 		epoch = rq->cpu_epoch;
 	}
 
@@ -1635,13 +1636,23 @@ static inline void __update_mm_sched(struct rq *rq,
 	}
 }
 
-static unsigned long fraction_mm_sched(struct rq *rq,
-				       struct sched_cache_time *pcpu_sched)
+static unsigned long fraction_mm_sched(int cpu,
+				       struct mm_struct *mm)
 {
+	struct sched_cache_time *pcpu_sched =
+		per_cpu_ptr(mm->sc_stat.pcpu_sched, cpu);
+	struct rq *rq = cpu_rq(cpu);
+
 	guard(raw_spinlock_irqsave)(&rq->cpu_epoch_lock);
 
 	__update_mm_sched(rq, pcpu_sched);
 
+	/* Skip the rq that has not been hit for a long time */
+	if ((rq->cpu_epoch - pcpu_sched->epoch_last_visit) > llc_epoch_affinity_timeout) {
+		cpumask_clear_cpu(cpu, mm->sc_stat.visited_cpus);
+		return 0;
+	}
+
 	/*
 	 * Runtime is a geometric series (r=0.5) and as such will sum to twice
 	 * the accumulation period, this means the multiplcation here should
@@ -1711,6 +1722,9 @@ void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
 		pcpu_sched->runtime += delta_exec;
 		rq->cpu_runtime += delta_exec;
 		epoch = rq->cpu_epoch;
+		pcpu_sched->epoch_last_visit = epoch;
+		if (!cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
+			cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
 	}
 
 	/*
@@ -1761,51 +1775,6 @@ static void task_tick_cache(struct rq *rq, struct task_struct *p)
 	}
 }
 
-static void get_scan_cpumasks(cpumask_var_t cpus, struct task_struct *p)
-{
-#ifdef CONFIG_NUMA_BALANCING
-	int cpu, curr_cpu, nid, pref_nid;
-
-	if (!static_branch_likely(&sched_numa_balancing))
-		goto out;
-
-	cpu = READ_ONCE(p->mm->sc_stat.cpu);
-	if (cpu != -1)
-		nid = cpu_to_node(cpu);
-	curr_cpu = task_cpu(p);
-
-	/*
-	 * Scanning in the preferred NUMA node is ideal. However, the NUMA
-	 * preferred node is per-task rather than per-process. It is possible
-	 * for different threads of the process to have distinct preferred
-	 * nodes; consequently, the process-wide preferred LLC may bounce
-	 * between different nodes. As a workaround, maintain the scan
-	 * CPU mask to also cover the process's current preferred LLC and the
-	 * current running node to mitigate the bouncing risk.
-	 * TBD: numa_group should be considered during task aggregation.
-	 */
-	pref_nid = p->numa_preferred_nid;
-	/* honor the task's preferred node */
-	if (pref_nid == NUMA_NO_NODE)
-		goto out;
-
-	cpumask_or(cpus, cpus, cpumask_of_node(pref_nid));
-
-	/* honor the task's preferred LLC CPU */
-	if (cpu != -1 && !cpumask_test_cpu(cpu, cpus) && nid != NUMA_NO_NODE)
-		cpumask_or(cpus, cpus, cpumask_of_node(nid));
-
-	/* make sure the task's current running node is included */
-	if (!cpumask_test_cpu(curr_cpu, cpus))
-		cpumask_or(cpus, cpus, cpumask_of_node(cpu_to_node(curr_cpu)));
-
-	return;
-
-out:
-#endif
-	cpumask_copy(cpus, cpu_online_mask);
-}
-
 static inline void update_avg_scale(u64 *avg, u64 sample)
 {
 	int factor = per_cpu(sd_llc_size, raw_smp_processor_id());
@@ -1845,7 +1814,7 @@ static void task_cache_work(struct callback_head *work)
 	if (time_before(now, next_scan))
 		return;
 
-	/* only 1 thread is allowed to scan */
+	/* elect a single scanner per epoch */
 	if (!try_cmpxchg(&mm->sc_stat.next_scan, &next_scan,
 			 now + max_t(unsigned long,
 				     READ_ONCE(llc_epoch_period), 1)))
@@ -1866,7 +1835,18 @@ static void task_cache_work(struct callback_head *work)
 	scoped_guard (cpus_read_lock) {
 		guard(rcu)();
 
-		get_scan_cpumasks(cpus, p);
+		/*
+		 * Data race: While evaluating the visited_cpus without
+		 * a lock, a CPU could be concurrently set by
+		 * account_mm_sched(), meaning the scan might skip the newly
+		 * visited CPU if the bit changes during the scan. This is
+		 * a deliberate trade-off between accuracy and efficiency:
+		 * locking would prevent this race but incur extra overhead.
+		 * The missed runtime contribution is negligible because it
+		 * implies this process hasn't run on that CPU for a long
+		 * time, and will be captured in the next cycle.
+		 */
+		cpumask_and(cpus, cpu_online_mask, mm->sc_stat.visited_cpus);
 
 		for_each_cpu(cpu, cpus) {
 			/* XXX sched_cluster_active */
@@ -1877,19 +1857,21 @@ static void task_cache_work(struct callback_head *work)
 			if (!sd)
 				continue;
 
-			for_each_cpu(i, sched_domain_span(sd)) {
-				occ = fraction_mm_sched(cpu_rq(i),
-						per_cpu_ptr(mm->sc_stat.pcpu_sched, i));
+			for_each_cpu_and(i, sched_domain_span(sd), cpus) {
+				cur = rcu_dereference_all(cpu_rq(i)->curr);
+				if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
+				    cur->mm == mm)
+					nr_running++;
+
+				occ = fraction_mm_sched(i, mm);
+				if (occ == 0)
+					continue;
+
 				a_occ += occ;
 				if (occ > m_occ) {
 					m_occ = occ;
 					m_cpu = i;
 				}
-
-				cur = rcu_dereference_all(cpu_rq(i)->curr);
-				if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
-				    cur->mm == mm)
-					nr_running++;
 			}
 
 			/*
-- 
2.34.1

① 数据结构的「记录 + 淘汰」基础

sched_cache_time 增加 epoch_last_visit,sched_cache_stat 增加 visited_cpus 位图(cpumask_var_t,动态分配避免 per-process 在 NR_CPUS 大时内存膨胀——这是 v6 相对 v5 的关键改动)。mm_alloc_sched_noprof() 负责 zalloc_cpumask_var(),mm_destroy_sched() 负责 free_cpumask_var(),配齐生命周期。

② 谁在「记录访问」:account_mm_sched()

进程在 rq 上运行并记账时,epoch_last_visit = epoch + cpumask_set_cpu(cpu_of(rq), visited_cpus)。关键:这是补丁唯一的「置位」点,且带 cpumask_test_cpu() 预检(v2 起加入)——CPU 已在位图里就不重复 set,避免 C2C(cache-to-cache)开销。

③ 淘汰 + 跳过:fraction_mm_sched() 改签名

函数签名从 (struct rq *rq, struct sched_cache_time *pcpu_sched) 改为 (int cpu, struct mm_struct *mm),内部自取 per_cpu_ptr() 与 cpu_rq()。加的超时判断是核心:(rq->cpu_epoch - pcpu_sched->epoch_last_visit) > llc_epoch_affinity_timeout 时清位图并返回 0——过期 CPU 不再贡献占用,下一次扫描也不会再进它。v4 引入的 epoch_timeout(后更名 epoch_last_visit)就是为此:单靠 epoch 不行,因为 fraction_mm_sched() 每次调用都会刷新 epoch,无法判断「多久没真访问」。

④ 只扫访问过的 CPU:task_cache_work() 收窄集合

外层 cpumask_and(cpus, cpu_online_mask, visited_cpus) 直接把扫描集合从「全在线 CPU」收窄成「访问过的 CPU」。内层 for_each_cpu_and(i, sched_domain_span(sd), cpus) 确保「即使跨节点 LLC 拓扑,也只处理访问过的 CPU」(v6 改动 2 的意图)。v8 删除 get_scan_cpumasks() 是因为 visited_cpus 已提供精确集合;v9 改用本地 cpus 副本是为了缩小读取竞态窗口(见下方 review)。cur / nr_running 统计移到 fraction_mm_sched() 之前并加 occ == 0 提前 continue,避免对淘汰 CPU 做无效累加。

📈 性能影响

场景/用例运行环境改进前改进后
Redis p99 延迟 @400K rps,NUMA balancing 关闭AMD 服务器,384 CPU0.554ms(cache-aware 无本系列,相对 baseline 恶化 -25.68%)0.441ms(-1.14% vs baseline,作者自报)
task_cache_work 周期占比(perf top -e cycles:k)同上0.81%0.02%(作者自报)
单次扫描 CPU 数(sched_cache_scan trace)同上38414 / 13 / 16(作者自报 trace)
Redis p99 @400K rps,NUMA balancing 开启同上0.454ms(-3.89% vs baseline)0.442ms(-1.14% vs baseline,作者自报)
task_cache_work 周期占比(NUMA balancing 开启)同上0.13%0.03%(作者自报)

说明:数据均来自 v9 cover letter,作者自报,未独立验证。基线 = 无 cache-aware 调度;「改进前」= 有 cache-aware 但无本系列(schedcache);「改进后」= 有 cache-aware + 本系列(schedcache_visit)。Hackbench 对比显示本系列对 cache-aware 调度精度基本无影响(多数配置 IMPROVED,个别配置小幅 REGRESSED,如 threads/2/2 组 -7.00%、threads/1/20 组相对 baseline -5.68%——后者是 cache-aware 相对 baseline 的回归,非本系列引入)。

on-CPU vs off-CPU 视角
  • on-CPU(计算侧)收益:本补丁减少的是 task_cache_work() 的指令/取锁/算术次数(384 → 14~16 个 rq),是典型的 on-CPU 开销降低。每 epoch 省下的就是「对从未访问的 rq 取 cpu_epoch_lock + 做几何级数累加」的时间,perf 占比 0.81% → 0.02% 直接体现。
  • off-CPU(等待侧)视角:间接收益——cpu_epoch_lock 是跨 rq 的锁,扫描 384 个 rq 会反复拿/放不同 CPU 的锁,与其他 CPU 的记账路径产生锁竞争;扫描集合变小后,锁足迹也随之缩小,间接缓解跨核锁争用(解读(AI 分析):补丁未提供 off-CPU/锁竞争数据,此项为逻辑推断)。
  • 对延迟的意义:Redis 这类延迟敏感负载,p99 从 -25.68% 回收到 -1.14%,本质是把「cache-aware 防 miss 带来的收益」从「被自身扫描开销吃掉」恢复出来。

🔄 方案演进 + 讨论焦点

时间线(v1 → v9)
  • v1(04-15 前):初版,引入 visited_cpus 概念 + 独立的 llc_epoch_visited_timeout,用 static key 控制。
  • v2(04-14):set/clear 前加 cpumask_test_cpu() 预检避免 C2C 开销;timeout 用 static key 优化。
  • v3(04-23):移除 static key、默认启用;复用 llc_epoch_affinity_timeout 替代新引入的 timeout;把 epoch 差计算移入 fraction_mm_sched() 避免与 __update_mm_sched() 的竞态;末尾重置 work->next 防同一进程多线程并发扫描。
  • v4(06-18):rebased 到 master;引入 epoch_timeout 淘汰过期 CPU(不能依赖 epoch,因为它被 fraction_mm_sched() 周期性刷新);nr_running 累加移到 fraction_mm_sched() 前;加 debug patch 展示扫描数。
  • v5(07-09):恢复 get_scan_cpumasks()(避免违反 NUMA_BALANCING 约束);用 for_each_cpu_and() 过滤 LLC 域内 CPU。
  • v6(07-17,任务指定 message-id):改用 cpumask_var_t 动态分配(防 NR_CPUS 大时 per-process 内存膨胀);LLC 域 span 直接与 visited_cpus 相交(防跨节点 LLC 拓扑下漏扫);epoch_timeout 更名 epoch_last_visit;__update_mm_sched() 移到超时判断前执行。
  • v7(07-20):在 task_cache_work() 加注释澄清 visited_cpus 设置/读取间的竞态。
  • v8(07-23,+46/-56):删除 get_scan_cpumasks()——visited_cpus 已提供精确扫描集合。
  • v9(07-31,当前版,+47/-57):内层循环改用 for_each_cpu_and(i, sched_domain_span(sd), cpus)(用本地副本而非直接读 visited_cpus),缩短读取 mm->sc_stat.pcpu_sched 的竞态窗口;更新注释为「每 epoch 选举单个扫描器」。
💬 讨论焦点(Intel cache-aware 系列作者参与)
  • Chen Yu(yu.c.chen@intel.com)v6 review(07-17):指出 visited_cpus 需要 zalloc_cpumask_var()/free_cpumask_var() 配对(v6 已具备);建议在 account_mm_sched() 或 task_cache_work() 加注释澄清竞态——"It's a trade-off between accuracy and efficiency - locking would fix it but at the cost of extra overhead IMO, and the up-to-date visited_cpus could be read properly in the next invoke of task_cache_work()"。来源:b86112e7。
  • Tim Chen(tim.c.chen@linux.intel.com)v7 review(07-20):提出最尖锐的竞态场景——CPU A 读 epoch_last_visit 看到「超时」→ 准备清位图;CPU B 恰好同时 account_mm_sched() 刷新 epoch_last_visit 并 set 位图;A 随后 cpumask_clear_cpu() 把 B 刚 set 的位抹掉,导致刚访问过的 CPU 被误删。他建议 smp_mb() + 双检,但自嘲 "This fix is ugly and there's probably a better way";同时建议既然 visited_cpus 已提供精确集合,可删除 get_scan_cpumasks()(v8 采纳)。来源:ee643d72。
  • Chen Yu 讨论收束(07-31):关于 task_tick_cache() 中 mm->sc_stat.epoch 的「avoid moving backwards」检查在锁外是否有效,Chen Yu 指出该逻辑本意是防 df0d98475954 的负超时差,且经 c1e7fe5e75ed 改为 (long) 强转后,epoch 略微回退「not a big deal」。这条讨论围绕的是既有特性的 epoch 语义,与本补丁的 visited_cpus 强相关(都在 epoch 语义上做超时判断)。来源:84f81335。
  • 未决:Tim Chen 提出的「读超时 → 清位图」与「并发刷新 + set」的竞态,作者用 v9 的本地副本 cpus + 注释声明「可接受的下一次捕获」来缓解,未采用 barrier 方案;是否会被维护者最终接受仍有待后续版本/讨论。

⚠️ 风险与局限

潜在回归 / 并发 / 边界
  • visited_cpus 竞态:Tim Chen 指出的「超时淘汰 vs 并发置位」竞态仍在——某 CPU 刚被进程跑到(set 位图),若扫描器读到旧 epoch_last_visit 判定超时并 clear,会把该 CPU 的贡献漏掉一个 epoch。作者判定影响可忽略(进程刚跑过的 CPU 下轮会再置位),但这是精度 vs 效率的取舍,不是严格正确。解读(AI 分析):漏掉一个 epoch 的贡献,只影响 pref_llc 选择延迟一个周期,不会造成错误迁移,风险可控。
  • NUMA balancing 交互:v5 曾恢复 get_scan_cpumasks() 以避免违反 NUMA_BALANCING 约束(scan 范围要覆盖进程的 preferred node / 当前运行 node),v8 又将其删除——删除后是否完整保留了 NUMA 语义、会不会在 NUMA balancing 开启时漏扫需要关注的 node,是 review 中值得关注的回归点(解读(AI 分析))。
  • 冷启动/足迹扩展:若进程的 CPU 足迹随时间扩大(迁移到新 node),visited_cpus 里只有旧 CPU,新 node 的 CPU 需等进程实际跑上去后才被纳入,pref_llc 更新可能滞后(解读(AI 分析):这是「只扫访问过的」设计固有代价)。
  • 多核扩展性:visited_cpus 是 per-mm 位图,扫描 O(visited),不随全系统核数增长而增长,扩展性反而更优;无新增全局串行化。
  • 内存:per-mm 动态分配 cpumask_var_t,进程数巨大时仍有额外内存,但 v6 改为动态分配已把 NR_CPUS 大时的固定膨胀消除。
  • Hackbench 回归:cover letter 中 schedcache vs schedcache_visit 有个别配置小幅 REGRESSED(如 threads/2/2 -7.00%、threads/2/4 -3.55%、threads/4/2 -2.68%),作者总体判定「不影响 cache-aware 调度精度」,但小配置下存在波动。
严重度:MINOR · review 质疑:Tim Chen 的 visited_cpus 竞态分析(ee643d72)已如实呈现;NUMA balancing 交互在 v5↔v8 间反复,是需维护者确认的点

🔗 交叉引用

📌 关联工作
df0d98475954 sched/cache: Introduce infrastructure for cache-aware load balancing — cache-aware 调度基础设施,本补丁优化的 task_cache_work() 由该系列引入(2026-04-01)
a26d9208c137 Merge branch 'sched/cache' — cache-aware 调度特性并入主线的合并点(2026-05-19)
Tim Chen v7 review — visited_cpus 竞态分析 + 建议删除 get_scan_cpumasks(Intel cache-aware 系列作者)
Chen Yu v6 review — zalloc/free_cpumask_var 配对 + 竞态取舍说明(Intel)
同站:sched/cache: honor migrate_llc_task semantics(08-03) — 同为 cache-aware 调度系列的另一条补丁(主动负载均衡路径语义补齐),可对照阅读
714059f79ff0 sched/cache: Handle moving single tasks to/from their preferred LLC — cache-aware 调度系列配套 commit

✅ 关键洞察

  • 发现:cache-aware 调度的周期性全量扫描是其在多 NUMA 大机上「防 miss 反而拖累延迟」的根因;本补丁用 per-mm visited_cpus 位图 + epoch_last_visit 超时淘汰,把扫描范围从全系统收敛到「进程真实访问过的 CPU」。
  • 证据:作者自报(未独立验证)——扫描 CPU 数 384→14~16,task_cache_work 周期占比 0.81%→0.02%(NUMA balancing 关闭)与 0.13%→0.03%(开启),Redis p99 从相对 baseline 恶化 -25.68% 回收到 -1.14%。
  • 边界:仅在 CONFIG_SCHED_CACHE 生效;进程 CPU 足迹远小于全系统 CPU 数的多实例场景收益最大;单 CPU 足迹≈全系统的场景(如均匀打散的高并发)收益趋近于无。
  • 风险 / 建议:Tim Chen 指出的「超时淘汰 vs 并发置位」竞态仍未根治(作者选择精度取舍 + 注释声明);v5↔v8 对 get_scan_cpumasks() 的去留反映 NUMA balancing 语义权衡,建议维护者确认删除后 NUMA 约束仍被满足;Hackbench 个别小配置有波动,建议补多节点大 CPU 数的扩展性验证。
⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。