* [PATCH v7 0/3] sched/fair: Optimize cfs_rq and sched_entity allocation for better data locality
@ 2026-01-18 3:34 Zecheng Li
2026-01-18 3:34 ` [PATCH v7 1/3] sched/fair: Co-locate cfs_rq and sched_entity in cfs_tg_state Zecheng Li
` (2 more replies)
0 siblings, 3 replies; 8+ messages in thread
From: Zecheng Li @ 2026-01-18 3:34 UTC (permalink / raw)
To: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, Rik van Riel, Chris Mason,
Madadi Vineeth Reddy, Xu Liu, Blake Jones, Josh Don, Nilay Vaish,
linux-kernel, Zecheng Li
Hi all,
This patch series improves CFS cache performance by allocating cfs_rq
and sched_entity together in the per-cpu allocator. It allows for
replacing the pointer arrays in task_group with a per-cpu offset.
v7:
- Removed struct sched_entity_stats, using cfs_tg_state to contain all
three structs: cfs_rq, sched_entity, and sched_statistics since they
are always allocated together in alloc_fair_sched_group.
v6:
- Rebased on latest tip/sched/core.
- Added my personal email as a secondary Signed-off-by.
- Note: I no longer have access to zecheng@google.com. I am picking
this series up from my personal address.
v5:
- Rebased on tip/sched/core (Resolved a minor conflict in patch 3)
- Friendly ping on the series as it's not receiving any comments
v4:
https://lore.kernel.org/all/20250903194503.1679687-1-zecheng@google.com/
- Rebased on tip/sched/core
- Intel kernel test robot results
https://lore.kernel.org/all/202507161052.ed3213f4-lkp@intel.com/
v3:
https://lore.kernel.org/all/20250701210230.2985885-1-zecheng@google.com/
- Rebased on top of 6.16-rc4.
- Minor wording and comment updates.
v2:
https://lore.kernel.org/lkml/20250609193834.2556866-1-zecheng@google.com/
- Allocate cfs_rq and sched_entity together for non-root task group
instead of embedding sched_entity into cfs_rq to avoid increasing the
size of struct rq based on the feedback from Peter Zijlstra.
v1:
https://lore.kernel.org/lkml/20250604195846.193159-1-zecheng@google.com/
Accessing cfs_rq and sched_entity instances incurs many cache misses.
This series of patches aims to reduce these cache misses. A struct
cfs_rq instance is per CPU and per task_group. Each task_group instance
(and the root runqueue) holds cfs_rq instances per CPU. Additionally,
there are corresponding struct sched_entity instances for each cfs_rq
instance (except the root). Currently, both cfs_rq and sched_entity
instances are allocated in NUMA-local memory using kzalloc_node, and
tg->cfs_rq and tg->se are arrays of pointers.
Original memory layout:
tg->cfs_rq = kcalloc(nr_cpu_ids, sizeof(cfs_rq), GFP_KERNEL);
tg->se = kcalloc(nr_cpu_ids, sizeof(se), GFP_KERNEL);
+----+ +-----------------+
| tg | ----> | cfs_rq pointers |
+----+ +-----------------+
| | |
v v v
cfs_rq cfs_rq cfs_rq
+----+ +--------------------+
| tg | ----> | sched_entity ptrs |
+----+ +--------------------+
| | |
v v v
se se se
Layout after Optimization:
+--------+ | CPU 0 | | CPU 1 | | CPU 2 |
| tg | | percpu | | percpu | | percpu |
| | ... ... ...
| percpu | -> | cfs_rq | | cfs_rq | | cfs_rq |
| offset | | se | | se | | se |
+--------+ +--------+ +--------+ +--------+
The optimization includes two parts:
1) Co-allocate cfs_rq and sched_entity for non-root task groups.
- This benefits loading the sched_entity for the parent runqueue.
Currently it incurs pointer chasing, i.e., cfs_rq->tg->se[cpu]. After
co-locating, the sched_entity fields can be loaded with simple offset
computations from cfs_rq.
2) Allocate the combined cfs_rq/se struct using percpu allocator.
- Accesses to cfs_rq instances in hot paths are mostly iterating through
multiple task_groups for the same CPU. Therefore, the new percpu
layout can reuse the base pointer, and they are more likely to reside
in the CPU cache than the per-task_group pointer arrays.
- This optimization also reduces the memory needed for the array of
pointers.
To measure the impact of the patch series, we construct a tree structure
hierarchy of cgroups, with “width” and “depth” parameters controlling
the number of children per node and the depth of the tree. Each leaf
cgroup runs a schbench workload and gets an 80% quota of the total CPU
quota divided by number of leaf cgroups (in other words, the target CPU
load is set to 80%) to exercise the throttling functions. Bandwidth
control period is set to 10ms. We run the benchmark on Intel and AMD
machines; each machine has hundreds of threads.
Tests were conducted on 6.15.
| Kernel LLC Misses | depth 3 width 10 | depth 5 width 4 |
+-------------------+---------------------+---------------------+
| AMD-orig | [2218.98, 2241.89]M | [2599.80, 2645.16]M |
| AMD-opt | [1957.62, 1981.55]M | [2380.47, 2431.86]M |
| Change | -11.69% | -8.248% |
| Intel-orig | [1580.53, 1604.90]M | [2125.37, 2208.68]M |
| Intel-opt | [1066.94, 1100.19]M | [1543.77, 1570.83]M |
| Change | -31.96% | -28.13% |
There's also a 25% improvement on kernel IPC on the AMD system. On
Intel, the improvement is 3% despite a greater LLC miss reduction.
Other workloads without CPU share limits, while also running in a cgroup
hierarchy with O(1000) instances, show no obvious regression:
sysbench, hackbench - lower is better; ebizzy - higher is better.
workload | base | opt | metric
----------+-----------------------+-----------------------+------------
sysbench | 63.55, [63.04, 64.05] | 64.36, [62.97, 65.75] | avg latency
hackbench | 36.95, [35.45, 38.45] | 37.12, [35.81, 38.44] | time
ebizzy | 610.7, [569.8, 651.6] | 613.5, [592.1, 635.0] | record/s
Zecheng Li (3):
sched/fair: Co-locate cfs_rq and sched_entity in cfs_tg_state
sched/fair: Remove task_group->se pointer array
sched/fair: Allocate both cfs_tg_state with percpu allocator
kernel/sched/core.c | 40 ++++++++-------------
kernel/sched/debug.c | 2 +-
kernel/sched/fair.c | 83 ++++++++++++++++----------------------------
kernel/sched/sched.h | 53 +++++++++++++++++++++++-----
kernel/sched/stats.h | 9 +----
5 files changed, 91 insertions(+), 96 deletions(-)
--
2.52.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v7 1/3] sched/fair: Co-locate cfs_rq and sched_entity in cfs_tg_state
2026-01-18 3:34 [PATCH v7 0/3] sched/fair: Optimize cfs_rq and sched_entity allocation for better data locality Zecheng Li
@ 2026-01-18 3:34 ` Zecheng Li
2026-01-19 6:40 ` K Prateek Nayak
2026-01-18 3:34 ` [PATCH v7 2/3] sched/fair: Remove task_group->se pointer array Zecheng Li
2026-01-18 3:34 ` [PATCH v7 3/3] sched/fair: Allocate both cfs_tg_state with percpu allocator Zecheng Li
2 siblings, 1 reply; 8+ messages in thread
From: Zecheng Li @ 2026-01-18 3:34 UTC (permalink / raw)
To: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, Rik van Riel, Chris Mason,
Madadi Vineeth Reddy, Xu Liu, Blake Jones, Josh Don, Nilay Vaish,
linux-kernel, Zecheng Li, Zecheng Li
From: Zecheng Li <zecheng@google.com>
Improve data locality and reduce pointer chasing by allocating struct
cfs_rq and struct sched_entity together for non-root task groups. This
is achieved by introducing a new combined struct cfs_tg_state that
holds both objects in a single allocation.
This patch:
- Introduces struct cfs_tg_state that embeds cfs_rq, sched_entity, and
sched_statistics together in a single structure.
- Updates __schedstats_from_se() in stats.h to use cfs_tg_state for
accessing sched_statistics from a group sched_entity.
- Modifies alloc_fair_sched_group() and free_fair_sched_group() to
allocate and free the new struct as a single unit.
- Modifies the per-CPU pointers in task_group->se and task_group->cfs_rq
to point to the members in the new combined structure.
Signed-off-by: Zecheng Li <zecheng@google.com>
Signed-off-by: Zecheng Li <zli94@ncsu.edu>
---
kernel/sched/fair.c | 25 +++++++++++--------------
kernel/sched/sched.h | 12 ++++++++++++
kernel/sched/stats.h | 9 +--------
3 files changed, 24 insertions(+), 22 deletions(-)
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 04993c763a06..0897dab69236 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -13617,10 +13617,11 @@ void free_fair_sched_group(struct task_group *tg)
int i;
for_each_possible_cpu(i) {
- if (tg->cfs_rq)
- kfree(tg->cfs_rq[i]);
- if (tg->se)
- kfree(tg->se[i]);
+ if (tg->cfs_rq && tg->cfs_rq[i]) {
+ struct cfs_tg_state *state =
+ container_of(tg->cfs_rq[i], struct cfs_tg_state, cfs_rq);
+ kfree(state);
+ }
}
kfree(tg->cfs_rq);
@@ -13629,6 +13630,7 @@ void free_fair_sched_group(struct task_group *tg)
int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)
{
+ struct cfs_tg_state *state;
struct sched_entity *se;
struct cfs_rq *cfs_rq;
int i;
@@ -13645,16 +13647,13 @@ int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)
init_cfs_bandwidth(tg_cfs_bandwidth(tg), tg_cfs_bandwidth(parent));
for_each_possible_cpu(i) {
- cfs_rq = kzalloc_node(sizeof(struct cfs_rq),
- GFP_KERNEL, cpu_to_node(i));
- if (!cfs_rq)
+ state = kzalloc_node(sizeof(*state),
+ GFP_KERNEL, cpu_to_node(i));
+ if (!state)
goto err;
- se = kzalloc_node(sizeof(struct sched_entity_stats),
- GFP_KERNEL, cpu_to_node(i));
- if (!se)
- goto err_free_rq;
-
+ cfs_rq = &state->cfs_rq;
+ se = &state->se;
init_cfs_rq(cfs_rq);
init_tg_cfs_entry(tg, cfs_rq, se, i, parent->se[i]);
init_entity_runnable_average(se);
@@ -13662,8 +13661,6 @@ int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)
return 1;
-err_free_rq:
- kfree(cfs_rq);
err:
return 0;
}
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 58c9d244f12b..50b37ed2f7d6 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -2191,6 +2191,18 @@ static inline struct task_group *task_group(struct task_struct *p)
return p->sched_task_group;
}
+#ifdef CONFIG_FAIR_GROUP_SCHED
+/*
+ * Defined here to be available before stats.h is included, since
+ * stats.h has dependencies on things defined later in this file.
+ */
+struct cfs_tg_state {
+ struct cfs_rq cfs_rq;
+ struct sched_entity se;
+ struct sched_statistics stats;
+} __no_randomize_layout;
+#endif
+
/* Change a task's cfs_rq and parent entity if it moves across CPUs/groups */
static inline void set_task_rq(struct task_struct *p, unsigned int cpu)
{
diff --git a/kernel/sched/stats.h b/kernel/sched/stats.h
index c903f1a42891..63b9a800a354 100644
--- a/kernel/sched/stats.h
+++ b/kernel/sched/stats.h
@@ -89,19 +89,12 @@ static inline void rq_sched_info_depart (struct rq *rq, unsigned long long delt
#endif /* CONFIG_SCHEDSTATS */
-#ifdef CONFIG_FAIR_GROUP_SCHED
-struct sched_entity_stats {
- struct sched_entity se;
- struct sched_statistics stats;
-} __no_randomize_layout;
-#endif
-
static inline struct sched_statistics *
__schedstats_from_se(struct sched_entity *se)
{
#ifdef CONFIG_FAIR_GROUP_SCHED
if (!entity_is_task(se))
- return &container_of(se, struct sched_entity_stats, se)->stats;
+ return &container_of(se, struct cfs_tg_state, se)->stats;
#endif
return &task_of(se)->stats;
}
--
2.52.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v7 2/3] sched/fair: Remove task_group->se pointer array
2026-01-18 3:34 [PATCH v7 0/3] sched/fair: Optimize cfs_rq and sched_entity allocation for better data locality Zecheng Li
2026-01-18 3:34 ` [PATCH v7 1/3] sched/fair: Co-locate cfs_rq and sched_entity in cfs_tg_state Zecheng Li
@ 2026-01-18 3:34 ` Zecheng Li
2026-01-19 6:43 ` K Prateek Nayak
2026-01-18 3:34 ` [PATCH v7 3/3] sched/fair: Allocate both cfs_tg_state with percpu allocator Zecheng Li
2 siblings, 1 reply; 8+ messages in thread
From: Zecheng Li @ 2026-01-18 3:34 UTC (permalink / raw)
To: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, Rik van Riel, Chris Mason,
Madadi Vineeth Reddy, Xu Liu, Blake Jones, Josh Don, Nilay Vaish,
linux-kernel, Zecheng Li, Zecheng Li
From: Zecheng Li <zecheng@google.com>
Now that struct sched_entity is co-located with struct cfs_rq for
non-root task groups, the task_group->se pointer array is redundant. The
associated sched_entity can be loaded directly from the cfs_rq.
This patch performs the access conversion with the helpers:
- is_root_task_group(tg): checks if a task group is the root task group.
It compares the task group's address with the global root_task_group
variable.
- tg_se(tg, cpu): retrieves the cfs_rq and returns the address of the
co-located se. This function checks if tg is the root task group to
ensure behaving the same of previous tg->se[cpu]. Replaces all accesses
that use the tg->se[cpu] pointer array with calls to the new tg_se(tg,
cpu) accessor.
- cfs_rq_se(cfs_rq): simplifies access paths like cfs_rq->tg->se[...] to
use the co-located sched_entity. This function also checks if tg is the
root task group to ensure same behavior.
Since tg_se is not in very hot code paths, and the branch is a register
comparison with an immediate value (`&root_task_group`), the performance
impact is expected to be negligible.
Signed-off-by: Zecheng Li <zecheng@google.com>
Signed-off-by: Zecheng Li <zli94@ncsu.edu>
---
kernel/sched/core.c | 7 ++-----
kernel/sched/debug.c | 2 +-
kernel/sched/fair.c | 25 +++++++++----------------
kernel/sched/sched.h | 29 ++++++++++++++++++++++++-----
4 files changed, 36 insertions(+), 27 deletions(-)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 3cca012d1259..8e2a67cecee9 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -8565,7 +8565,7 @@ void __init sched_init(void)
wait_bit_init();
#ifdef CONFIG_FAIR_GROUP_SCHED
- ptr += 2 * nr_cpu_ids * sizeof(void **);
+ ptr += nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_RT_GROUP_SCHED
ptr += 2 * nr_cpu_ids * sizeof(void **);
@@ -8574,9 +8574,6 @@ void __init sched_init(void)
ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT);
#ifdef CONFIG_FAIR_GROUP_SCHED
- root_task_group.se = (struct sched_entity **)ptr;
- ptr += nr_cpu_ids * sizeof(void **);
-
root_task_group.cfs_rq = (struct cfs_rq **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
@@ -9644,7 +9641,7 @@ static int cpu_cfs_stat_show(struct seq_file *sf, void *v)
int i;
for_each_possible_cpu(i) {
- stats = __schedstats_from_se(tg->se[i]);
+ stats = __schedstats_from_se(tg_se(tg, i));
ws += schedstat_val(stats->wait_sum);
}
diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
index 5f9b77195159..544d9ae4e0df 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -644,7 +644,7 @@ void dirty_sched_domain_sysctl(int cpu)
#ifdef CONFIG_FAIR_GROUP_SCHED
static void print_cfs_group_stats(struct seq_file *m, int cpu, struct task_group *tg)
{
- struct sched_entity *se = tg->se[cpu];
+ struct sched_entity *se = tg_se(tg, cpu);
#define P(F) SEQ_printf(m, " .%-30s: %lld\n", #F, (long long)F)
#define P_SCHEDSTAT(F) SEQ_printf(m, " .%-30s: %lld\n", \
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 0897dab69236..e44bd5448fa5 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -5971,7 +5971,7 @@ void unthrottle_cfs_rq(struct cfs_rq *cfs_rq)
{
struct rq *rq = rq_of(cfs_rq);
struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(cfs_rq->tg);
- struct sched_entity *se = cfs_rq->tg->se[cpu_of(rq)];
+ struct sched_entity *se = cfs_rq_se(cfs_rq);
/*
* It's possible we are called with runtime_remaining < 0 due to things
@@ -9839,7 +9839,6 @@ static bool __update_blocked_fair(struct rq *rq, bool *done)
{
struct cfs_rq *cfs_rq, *pos;
bool decayed = false;
- int cpu = cpu_of(rq);
/*
* Iterates the task_group tree in a bottom up fashion, see
@@ -9859,7 +9858,7 @@ static bool __update_blocked_fair(struct rq *rq, bool *done)
}
/* Propagate pending load changes to the parent, if any: */
- se = cfs_rq->tg->se[cpu];
+ se = cfs_rq_se(cfs_rq);
if (se && !skip_blocked_update(se))
update_load_avg(cfs_rq_of(se), se, UPDATE_TG);
@@ -9885,8 +9884,7 @@ static bool __update_blocked_fair(struct rq *rq, bool *done)
*/
static void update_cfs_rq_h_load(struct cfs_rq *cfs_rq)
{
- struct rq *rq = rq_of(cfs_rq);
- struct sched_entity *se = cfs_rq->tg->se[cpu_of(rq)];
+ struct sched_entity *se = cfs_rq_se(cfs_rq);
unsigned long now = jiffies;
unsigned long load;
@@ -13625,7 +13623,6 @@ void free_fair_sched_group(struct task_group *tg)
}
kfree(tg->cfs_rq);
- kfree(tg->se);
}
int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)
@@ -13638,9 +13635,6 @@ int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)
tg->cfs_rq = kcalloc(nr_cpu_ids, sizeof(cfs_rq), GFP_KERNEL);
if (!tg->cfs_rq)
goto err;
- tg->se = kcalloc(nr_cpu_ids, sizeof(se), GFP_KERNEL);
- if (!tg->se)
- goto err;
tg->shares = NICE_0_LOAD;
@@ -13655,7 +13649,7 @@ int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)
cfs_rq = &state->cfs_rq;
se = &state->se;
init_cfs_rq(cfs_rq);
- init_tg_cfs_entry(tg, cfs_rq, se, i, parent->se[i]);
+ init_tg_cfs_entry(tg, cfs_rq, se, i, tg_se(parent, i));
init_entity_runnable_average(se);
}
@@ -13674,7 +13668,7 @@ void online_fair_sched_group(struct task_group *tg)
for_each_possible_cpu(i) {
rq = cpu_rq(i);
- se = tg->se[i];
+ se = tg_se(tg, i);
rq_lock_irq(rq, &rf);
update_rq_clock(rq);
attach_entity_cfs_rq(se);
@@ -13691,7 +13685,7 @@ void unregister_fair_sched_group(struct task_group *tg)
for_each_possible_cpu(cpu) {
struct cfs_rq *cfs_rq = tg->cfs_rq[cpu];
- struct sched_entity *se = tg->se[cpu];
+ struct sched_entity *se = tg_se(tg, cpu);
struct rq *rq = cpu_rq(cpu);
if (se) {
@@ -13728,7 +13722,6 @@ void init_tg_cfs_entry(struct task_group *tg, struct cfs_rq *cfs_rq,
init_cfs_rq_runtime(cfs_rq);
tg->cfs_rq[cpu] = cfs_rq;
- tg->se[cpu] = se;
/* se could be NULL for root_task_group */
if (!se)
@@ -13759,7 +13752,7 @@ static int __sched_group_set_shares(struct task_group *tg, unsigned long shares)
/*
* We can't change the weight of the root cgroup.
*/
- if (!tg->se[0])
+ if (is_root_task_group(tg))
return -EINVAL;
shares = clamp(shares, scale_load(MIN_SHARES), scale_load(MAX_SHARES));
@@ -13770,7 +13763,7 @@ static int __sched_group_set_shares(struct task_group *tg, unsigned long shares)
tg->shares = shares;
for_each_possible_cpu(i) {
struct rq *rq = cpu_rq(i);
- struct sched_entity *se = tg->se[i];
+ struct sched_entity *se = tg_se(tg, i);
struct rq_flags rf;
/* Propagate contribution to hierarchy */
@@ -13821,7 +13814,7 @@ int sched_group_set_idle(struct task_group *tg, long idle)
for_each_possible_cpu(i) {
struct rq *rq = cpu_rq(i);
- struct sched_entity *se = tg->se[i];
+ struct sched_entity *se = tg_se(tg, i);
struct cfs_rq *grp_cfs_rq = tg->cfs_rq[i];
bool was_idle = cfs_rq_is_idle(grp_cfs_rq);
long idle_task_delta;
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 50b37ed2f7d6..bb6daf2c1e79 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -476,8 +476,6 @@ struct task_group {
#endif
#ifdef CONFIG_FAIR_GROUP_SCHED
- /* schedulable entities of this group on each CPU */
- struct sched_entity **se;
/* runqueue "owned" by this group on each CPU */
struct cfs_rq **cfs_rq;
unsigned long shares;
@@ -915,7 +913,8 @@ struct dl_rq {
};
#ifdef CONFIG_FAIR_GROUP_SCHED
-
+/* Check whether a task group is root tg */
+#define is_root_task_group(tg) ((tg) == &root_task_group)
/* An entity is a task if it doesn't "own" a runqueue */
#define entity_is_task(se) (!se->my_q)
@@ -2201,6 +2200,26 @@ struct cfs_tg_state {
struct sched_entity se;
struct sched_statistics stats;
} __no_randomize_layout;
+
+static inline struct sched_entity *tg_se(struct task_group *tg, int cpu)
+{
+ if (is_root_task_group(tg))
+ return NULL;
+
+ struct cfs_tg_state *state =
+ container_of(tg->cfs_rq[cpu], struct cfs_tg_state, cfs_rq);
+ return &state->se;
+}
+
+static inline struct sched_entity *cfs_rq_se(struct cfs_rq *cfs_rq)
+{
+ if (is_root_task_group(cfs_rq->tg))
+ return NULL;
+
+ struct cfs_tg_state *state =
+ container_of(cfs_rq, struct cfs_tg_state, cfs_rq);
+ return &state->se;
+}
#endif
/* Change a task's cfs_rq and parent entity if it moves across CPUs/groups */
@@ -2213,8 +2232,8 @@ static inline void set_task_rq(struct task_struct *p, unsigned int cpu)
#ifdef CONFIG_FAIR_GROUP_SCHED
set_task_rq_fair(&p->se, p->se.cfs_rq, tg->cfs_rq[cpu]);
p->se.cfs_rq = tg->cfs_rq[cpu];
- p->se.parent = tg->se[cpu];
- p->se.depth = tg->se[cpu] ? tg->se[cpu]->depth + 1 : 0;
+ p->se.parent = tg_se(tg, cpu);
+ p->se.depth = p->se.parent ? p->se.parent->depth + 1 : 0;
#endif
#ifdef CONFIG_RT_GROUP_SCHED
--
2.52.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v7 3/3] sched/fair: Allocate both cfs_tg_state with percpu allocator
2026-01-18 3:34 [PATCH v7 0/3] sched/fair: Optimize cfs_rq and sched_entity allocation for better data locality Zecheng Li
2026-01-18 3:34 ` [PATCH v7 1/3] sched/fair: Co-locate cfs_rq and sched_entity in cfs_tg_state Zecheng Li
2026-01-18 3:34 ` [PATCH v7 2/3] sched/fair: Remove task_group->se pointer array Zecheng Li
@ 2026-01-18 3:34 ` Zecheng Li
2026-01-19 7:17 ` K Prateek Nayak
2 siblings, 1 reply; 8+ messages in thread
From: Zecheng Li @ 2026-01-18 3:34 UTC (permalink / raw)
To: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, Rik van Riel, Chris Mason,
Madadi Vineeth Reddy, Xu Liu, Blake Jones, Josh Don, Nilay Vaish,
linux-kernel, Zecheng Li, Zecheng Li
From: Zecheng Li <zecheng@google.com>
To remove the cfs_rq pointer array in task_group, allocate the combined
cfs_rq and sched_entity using the per-cpu allocator.
This patch implements the following:
- Changes task_group->cfs_rq from struct cfs_rq ** to struct cfs_rq
__percpu *.
- Updates memory allocation in alloc_fair_sched_group() and
free_fair_sched_group() to use alloc_percpu() and free_percpu()
respectively.
- Uses the inline accessor tg_cfs_rq(tg, cpu) with per_cpu_ptr() to
retrieve the pointer to cfs_rq for the given task group and CPU.
- Replaces direct accesses tg->cfs_rq[cpu] with calls to the new
tg_cfs_rq(tg, cpu) helper.
- Handles the root_task_group: since struct rq is already a per-cpu
variable (runqueues), its embedded cfs_rq (rq->cfs) is also per-cpu.
Therefore, we assign root_task_group.cfs_rq = &runqueues.cfs.
- Cleanup the code in initializing the root task group.
This change places each CPU's cfs_rq and sched_entity in its local
per-cpu memory area to remove the per-task_group pointer arrays.
Signed-off-by: Zecheng Li <zecheng@google.com>
Signed-off-by: Zecheng Li <zli94@ncsu.edu>
---
kernel/sched/core.c | 35 ++++++++++-----------------
kernel/sched/fair.c | 57 +++++++++++++++++---------------------------
kernel/sched/sched.h | 14 +++++++----
3 files changed, 45 insertions(+), 61 deletions(-)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 8e2a67cecee9..80e8f4eb3f87 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -8549,7 +8549,7 @@ static struct kmem_cache *task_group_cache __ro_after_init;
void __init sched_init(void)
{
- unsigned long ptr = 0;
+ unsigned long __maybe_unused ptr = 0;
int i;
/* Make sure the linker didn't screw up */
@@ -8565,33 +8565,24 @@ void __init sched_init(void)
wait_bit_init();
#ifdef CONFIG_FAIR_GROUP_SCHED
- ptr += nr_cpu_ids * sizeof(void **);
-#endif
-#ifdef CONFIG_RT_GROUP_SCHED
- ptr += 2 * nr_cpu_ids * sizeof(void **);
-#endif
- if (ptr) {
- ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT);
+ root_task_group.cfs_rq = &runqueues.cfs;
-#ifdef CONFIG_FAIR_GROUP_SCHED
- root_task_group.cfs_rq = (struct cfs_rq **)ptr;
- ptr += nr_cpu_ids * sizeof(void **);
-
- root_task_group.shares = ROOT_TASK_GROUP_LOAD;
- init_cfs_bandwidth(&root_task_group.cfs_bandwidth, NULL);
+ root_task_group.shares = ROOT_TASK_GROUP_LOAD;
+ init_cfs_bandwidth(&root_task_group.cfs_bandwidth, NULL);
#endif /* CONFIG_FAIR_GROUP_SCHED */
#ifdef CONFIG_EXT_GROUP_SCHED
- scx_tg_init(&root_task_group);
+ scx_tg_init(&root_task_group);
#endif /* CONFIG_EXT_GROUP_SCHED */
#ifdef CONFIG_RT_GROUP_SCHED
- root_task_group.rt_se = (struct sched_rt_entity **)ptr;
- ptr += nr_cpu_ids * sizeof(void **);
+ ptr += 2 * nr_cpu_ids * sizeof(void **);
+ ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT);
+ root_task_group.rt_se = (struct sched_rt_entity **)ptr;
+ ptr += nr_cpu_ids * sizeof(void **);
- root_task_group.rt_rq = (struct rt_rq **)ptr;
- ptr += nr_cpu_ids * sizeof(void **);
+ root_task_group.rt_rq = (struct rt_rq **)ptr;
+ ptr += nr_cpu_ids * sizeof(void **);
#endif /* CONFIG_RT_GROUP_SCHED */
- }
init_defrootdomain();
@@ -9492,7 +9483,7 @@ static int tg_set_cfs_bandwidth(struct task_group *tg,
}
for_each_online_cpu(i) {
- struct cfs_rq *cfs_rq = tg->cfs_rq[i];
+ struct cfs_rq *cfs_rq = tg_cfs_rq(tg, i);
struct rq *rq = cfs_rq->rq;
guard(rq_lock_irq)(rq);
@@ -9660,7 +9651,7 @@ static u64 throttled_time_self(struct task_group *tg)
u64 total = 0;
for_each_possible_cpu(i) {
- total += READ_ONCE(tg->cfs_rq[i]->throttled_clock_self_time);
+ total += READ_ONCE(tg_cfs_rq(tg, i)->throttled_clock_self_time);
}
return total;
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index e44bd5448fa5..bc023704acd1 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -327,7 +327,7 @@ static inline bool list_add_leaf_cfs_rq(struct cfs_rq *cfs_rq)
* to a tree or when we reach the top of the tree
*/
if (cfs_rq->tg->parent &&
- cfs_rq->tg->parent->cfs_rq[cpu]->on_list) {
+ tg_cfs_rq(cfs_rq->tg->parent, cpu)->on_list) {
/*
* If parent is already on the list, we add the child
* just before. Thanks to circular linked property of
@@ -335,7 +335,7 @@ static inline bool list_add_leaf_cfs_rq(struct cfs_rq *cfs_rq)
* of the list that starts by parent.
*/
list_add_tail_rcu(&cfs_rq->leaf_cfs_rq_list,
- &(cfs_rq->tg->parent->cfs_rq[cpu]->leaf_cfs_rq_list));
+ &(tg_cfs_rq(cfs_rq->tg->parent, cpu)->leaf_cfs_rq_list));
/*
* The branch is now connected to its tree so we can
* reset tmp_alone_branch to the beginning of the
@@ -4153,7 +4153,7 @@ static void __maybe_unused clear_tg_offline_cfs_rqs(struct rq *rq)
rcu_read_lock();
list_for_each_entry_rcu(tg, &task_groups, list) {
- struct cfs_rq *cfs_rq = tg->cfs_rq[cpu_of(rq)];
+ struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu_of(rq));
clear_tg_load_avg(cfs_rq);
}
@@ -5689,7 +5689,7 @@ static inline int throttled_hierarchy(struct cfs_rq *cfs_rq)
static inline int lb_throttled_hierarchy(struct task_struct *p, int dst_cpu)
{
- return throttled_hierarchy(task_group(p)->cfs_rq[dst_cpu]);
+ return throttled_hierarchy(tg_cfs_rq(task_group(p), dst_cpu));
}
static inline bool task_is_throttled(struct task_struct *p)
@@ -5835,7 +5835,7 @@ static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags);
static int tg_unthrottle_up(struct task_group *tg, void *data)
{
struct rq *rq = data;
- struct cfs_rq *cfs_rq = tg->cfs_rq[cpu_of(rq)];
+ struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu_of(rq));
struct task_struct *p, *tmp;
if (--cfs_rq->throttle_count)
@@ -5906,7 +5906,7 @@ static void record_throttle_clock(struct cfs_rq *cfs_rq)
static int tg_throttle_down(struct task_group *tg, void *data)
{
struct rq *rq = data;
- struct cfs_rq *cfs_rq = tg->cfs_rq[cpu_of(rq)];
+ struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu_of(rq));
if (cfs_rq->throttle_count++)
return 0;
@@ -6379,8 +6379,8 @@ static void sync_throttle(struct task_group *tg, int cpu)
if (!tg->parent)
return;
- cfs_rq = tg->cfs_rq[cpu];
- pcfs_rq = tg->parent->cfs_rq[cpu];
+ cfs_rq = tg_cfs_rq(tg, cpu);
+ pcfs_rq = tg_cfs_rq(tg->parent, cpu);
cfs_rq->throttle_count = pcfs_rq->throttle_count;
cfs_rq->throttled_clock_pelt = rq_clock_pelt(cpu_rq(cpu));
@@ -6572,7 +6572,7 @@ static void __maybe_unused update_runtime_enabled(struct rq *rq)
rcu_read_lock();
list_for_each_entry_rcu(tg, &task_groups, list) {
struct cfs_bandwidth *cfs_b = &tg->cfs_bandwidth;
- struct cfs_rq *cfs_rq = tg->cfs_rq[cpu_of(rq)];
+ struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu_of(rq));
raw_spin_lock(&cfs_b->lock);
cfs_rq->runtime_enabled = cfs_b->quota != RUNTIME_INF;
@@ -6601,7 +6601,7 @@ static void __maybe_unused unthrottle_offline_cfs_rqs(struct rq *rq)
rcu_read_lock();
list_for_each_entry_rcu(tg, &task_groups, list) {
- struct cfs_rq *cfs_rq = tg->cfs_rq[cpu_of(rq)];
+ struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu_of(rq));
if (!cfs_rq->runtime_enabled)
continue;
@@ -9408,7 +9408,7 @@ static inline int task_is_ineligible_on_dst_cpu(struct task_struct *p, int dest_
struct cfs_rq *dst_cfs_rq;
#ifdef CONFIG_FAIR_GROUP_SCHED
- dst_cfs_rq = task_group(p)->cfs_rq[dest_cpu];
+ dst_cfs_rq = tg_cfs_rq(task_group(p), dest_cpu);
#else
dst_cfs_rq = &cpu_rq(dest_cpu)->cfs;
#endif
@@ -13346,7 +13346,7 @@ static int task_is_throttled_fair(struct task_struct *p, int cpu)
struct cfs_rq *cfs_rq;
#ifdef CONFIG_FAIR_GROUP_SCHED
- cfs_rq = task_group(p)->cfs_rq[cpu];
+ cfs_rq = tg_cfs_rq(task_group(p), cpu);
#else
cfs_rq = &cpu_rq(cpu)->cfs;
#endif
@@ -13612,42 +13612,31 @@ static void task_change_group_fair(struct task_struct *p)
void free_fair_sched_group(struct task_group *tg)
{
- int i;
-
- for_each_possible_cpu(i) {
- if (tg->cfs_rq && tg->cfs_rq[i]) {
- struct cfs_tg_state *state =
- container_of(tg->cfs_rq[i], struct cfs_tg_state, cfs_rq);
- kfree(state);
- }
- }
-
- kfree(tg->cfs_rq);
+ free_percpu(tg->cfs_rq);
}
int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)
{
- struct cfs_tg_state *state;
+ struct cfs_tg_state __percpu *state;
struct sched_entity *se;
struct cfs_rq *cfs_rq;
int i;
- tg->cfs_rq = kcalloc(nr_cpu_ids, sizeof(cfs_rq), GFP_KERNEL);
- if (!tg->cfs_rq)
+ state = alloc_percpu_gfp(struct cfs_tg_state, GFP_KERNEL);
+ if (!state)
goto err;
+ tg->cfs_rq = &state->cfs_rq;
tg->shares = NICE_0_LOAD;
init_cfs_bandwidth(tg_cfs_bandwidth(tg), tg_cfs_bandwidth(parent));
for_each_possible_cpu(i) {
- state = kzalloc_node(sizeof(*state),
- GFP_KERNEL, cpu_to_node(i));
- if (!state)
+ cfs_rq = tg_cfs_rq(tg, i);
+ if (!cfs_rq)
goto err;
- cfs_rq = &state->cfs_rq;
- se = &state->se;
+ se = tg_se(tg, i);
init_cfs_rq(cfs_rq);
init_tg_cfs_entry(tg, cfs_rq, se, i, tg_se(parent, i));
init_entity_runnable_average(se);
@@ -13684,7 +13673,7 @@ void unregister_fair_sched_group(struct task_group *tg)
destroy_cfs_bandwidth(tg_cfs_bandwidth(tg));
for_each_possible_cpu(cpu) {
- struct cfs_rq *cfs_rq = tg->cfs_rq[cpu];
+ struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu);
struct sched_entity *se = tg_se(tg, cpu);
struct rq *rq = cpu_rq(cpu);
@@ -13721,8 +13710,6 @@ void init_tg_cfs_entry(struct task_group *tg, struct cfs_rq *cfs_rq,
cfs_rq->rq = rq;
init_cfs_rq_runtime(cfs_rq);
- tg->cfs_rq[cpu] = cfs_rq;
-
/* se could be NULL for root_task_group */
if (!se)
return;
@@ -13815,7 +13802,7 @@ int sched_group_set_idle(struct task_group *tg, long idle)
for_each_possible_cpu(i) {
struct rq *rq = cpu_rq(i);
struct sched_entity *se = tg_se(tg, i);
- struct cfs_rq *grp_cfs_rq = tg->cfs_rq[i];
+ struct cfs_rq *grp_cfs_rq = tg_cfs_rq(tg, i);
bool was_idle = cfs_rq_is_idle(grp_cfs_rq);
long idle_task_delta;
struct rq_flags rf;
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index bb6daf2c1e79..8ad1780edfe8 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -477,7 +477,7 @@ struct task_group {
#ifdef CONFIG_FAIR_GROUP_SCHED
/* runqueue "owned" by this group on each CPU */
- struct cfs_rq **cfs_rq;
+ struct cfs_rq __percpu *cfs_rq;
unsigned long shares;
/*
* load_avg can be heavily contended at clock tick time, so put
@@ -2201,13 +2201,19 @@ struct cfs_tg_state {
struct sched_statistics stats;
} __no_randomize_layout;
+/* Access a specific CPU's cfs_rq from a task group */
+static inline struct cfs_rq *tg_cfs_rq(struct task_group *tg, int cpu)
+{
+ return per_cpu_ptr(tg->cfs_rq, cpu);
+}
+
static inline struct sched_entity *tg_se(struct task_group *tg, int cpu)
{
if (is_root_task_group(tg))
return NULL;
struct cfs_tg_state *state =
- container_of(tg->cfs_rq[cpu], struct cfs_tg_state, cfs_rq);
+ container_of(tg_cfs_rq(tg, cpu), struct cfs_tg_state, cfs_rq);
return &state->se;
}
@@ -2230,8 +2236,8 @@ static inline void set_task_rq(struct task_struct *p, unsigned int cpu)
#endif
#ifdef CONFIG_FAIR_GROUP_SCHED
- set_task_rq_fair(&p->se, p->se.cfs_rq, tg->cfs_rq[cpu]);
- p->se.cfs_rq = tg->cfs_rq[cpu];
+ set_task_rq_fair(&p->se, p->se.cfs_rq, tg_cfs_rq(tg, cpu));
+ p->se.cfs_rq = tg_cfs_rq(tg, cpu);
p->se.parent = tg_se(tg, cpu);
p->se.depth = p->se.parent ? p->se.parent->depth + 1 : 0;
#endif
--
2.52.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v7 1/3] sched/fair: Co-locate cfs_rq and sched_entity in cfs_tg_state
2026-01-18 3:34 ` [PATCH v7 1/3] sched/fair: Co-locate cfs_rq and sched_entity in cfs_tg_state Zecheng Li
@ 2026-01-19 6:40 ` K Prateek Nayak
0 siblings, 0 replies; 8+ messages in thread
From: K Prateek Nayak @ 2026-01-19 6:40 UTC (permalink / raw)
To: Zecheng Li, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, Rik van Riel, Chris Mason,
Madadi Vineeth Reddy, Xu Liu, Blake Jones, Josh Don, Nilay Vaish,
linux-kernel, Zecheng Li
Hello Zecheng,
On 1/18/2026 9:04 AM, Zecheng Li wrote:
> @@ -13617,10 +13617,11 @@ void free_fair_sched_group(struct task_group *tg)
> int i;
>
> for_each_possible_cpu(i) {
> - if (tg->cfs_rq)
> - kfree(tg->cfs_rq[i]);
> - if (tg->se)
> - kfree(tg->se[i]);
> + if (tg->cfs_rq && tg->cfs_rq[i]) {
nit. Since the cfs_tg_state uses __no_randomize_layout now and, "cfs_rq"
is the first member, you can just do kfree(tg->cfs_rq[i]) since it is
the beginning of cfs_tg_state too.
This is how kfree() on tg->se[i] would have freed the entire
"sched_entity_stats" previously.
I know this is a transient change since later you switch to using
free_percpu() but it would still be nice to have a simplified hunk here.
> + struct cfs_tg_state *state =
> + container_of(tg->cfs_rq[i], struct cfs_tg_state, cfs_rq);
> + kfree(state);
> + }
> }
>
> kfree(tg->cfs_rq);
--
Thanks and Regards,
Prateek
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v7 2/3] sched/fair: Remove task_group->se pointer array
2026-01-18 3:34 ` [PATCH v7 2/3] sched/fair: Remove task_group->se pointer array Zecheng Li
@ 2026-01-19 6:43 ` K Prateek Nayak
0 siblings, 0 replies; 8+ messages in thread
From: K Prateek Nayak @ 2026-01-19 6:43 UTC (permalink / raw)
To: Zecheng Li, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, Rik van Riel, Chris Mason,
Madadi Vineeth Reddy, Xu Liu, Blake Jones, Josh Don, Nilay Vaish,
linux-kernel, Zecheng Li
Hello Zecheng,
On 1/18/2026 9:04 AM, Zecheng Li wrote:
> @@ -2201,6 +2200,26 @@ struct cfs_tg_state {
> struct sched_entity se;
> struct sched_statistics stats;
> } __no_randomize_layout;
> +
> +static inline struct sched_entity *tg_se(struct task_group *tg, int cpu)
> +{
> + if (is_root_task_group(tg))
> + return NULL;
> +
> + struct cfs_tg_state *state =
nit. Can you move the declaration of "struct cfs_tg_state *state" to the
top of the function. Same for cfs_rq_se().
> + container_of(tg->cfs_rq[cpu], struct cfs_tg_state, cfs_rq);
> + return &state->se;
> +}
> +
> +static inline struct sched_entity *cfs_rq_se(struct cfs_rq *cfs_rq)
> +{
> + if (is_root_task_group(cfs_rq->tg))
> + return NULL;
> +
> + struct cfs_tg_state *state =
> + container_of(cfs_rq, struct cfs_tg_state, cfs_rq);
> + return &state->se;
> +}
> #endif
--
Thanks and Regards,
Prateek
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v7 3/3] sched/fair: Allocate both cfs_tg_state with percpu allocator
2026-01-18 3:34 ` [PATCH v7 3/3] sched/fair: Allocate both cfs_tg_state with percpu allocator Zecheng Li
@ 2026-01-19 7:17 ` K Prateek Nayak
2026-01-20 19:11 ` Zecheng Li
0 siblings, 1 reply; 8+ messages in thread
From: K Prateek Nayak @ 2026-01-19 7:17 UTC (permalink / raw)
To: Zecheng Li, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, Rik van Riel, Chris Mason,
Madadi Vineeth Reddy, Xu Liu, Blake Jones, Josh Don, Nilay Vaish,
linux-kernel, Zecheng Li
Hello Zecheng,
On 1/18/2026 9:04 AM, Zecheng Li wrote:
> --- a/kernel/sched/core.c
> +++ b/kernel/sched/core.c
> @@ -8549,7 +8549,7 @@ static struct kmem_cache *task_group_cache __ro_after_init;
>
> void __init sched_init(void)
> {
> - unsigned long ptr = 0;
> + unsigned long __maybe_unused ptr = 0;
Since "ptr" is now only used for CONFIG_RT_GROUP_SCHED ...
> int i;
>
> /* Make sure the linker didn't screw up */
> @@ -8565,33 +8565,24 @@ void __init sched_init(void)
> wait_bit_init();
>
> #ifdef CONFIG_FAIR_GROUP_SCHED
> - ptr += nr_cpu_ids * sizeof(void **);
> -#endif
> -#ifdef CONFIG_RT_GROUP_SCHED
> - ptr += 2 * nr_cpu_ids * sizeof(void **);
> -#endif
> - if (ptr) {
> - ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT);
> + root_task_group.cfs_rq = &runqueues.cfs;
>
> -#ifdef CONFIG_FAIR_GROUP_SCHED
> - root_task_group.cfs_rq = (struct cfs_rq **)ptr;
> - ptr += nr_cpu_ids * sizeof(void **);
> -
> - root_task_group.shares = ROOT_TASK_GROUP_LOAD;
> - init_cfs_bandwidth(&root_task_group.cfs_bandwidth, NULL);
> + root_task_group.shares = ROOT_TASK_GROUP_LOAD;
> + init_cfs_bandwidth(&root_task_group.cfs_bandwidth, NULL);
> #endif /* CONFIG_FAIR_GROUP_SCHED */
> #ifdef CONFIG_EXT_GROUP_SCHED
> - scx_tg_init(&root_task_group);
> + scx_tg_init(&root_task_group);
> #endif /* CONFIG_EXT_GROUP_SCHED */
> #ifdef CONFIG_RT_GROUP_SCHED
> - root_task_group.rt_se = (struct sched_rt_entity **)ptr;
> - ptr += nr_cpu_ids * sizeof(void **);
> + ptr += 2 * nr_cpu_ids * sizeof(void **);
> + ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT);
> + root_task_group.rt_se = (struct sched_rt_entity **)ptr;
> + ptr += nr_cpu_ids * sizeof(void **);
>
> - root_task_group.rt_rq = (struct rt_rq **)ptr;
> - ptr += nr_cpu_ids * sizeof(void **);
> + root_task_group.rt_rq = (struct rt_rq **)ptr;
> + ptr += nr_cpu_ids * sizeof(void **);
Can we also optimize the CONFIG_RT_GROUP_SCHED stuff in the same way
although I'm assuming the benefit is far less since they aren't
accessed as frequently as the cfs bits. Something like:
(Only build and boot tested on top of your series)
diff --git a/kernel/sched/autogroup.c b/kernel/sched/autogroup.c
index 954137775f38..7135f19d5fa2 100644
--- a/kernel/sched/autogroup.c
+++ b/kernel/sched/autogroup.c
@@ -52,7 +52,6 @@ static inline void autogroup_destroy(struct kref *kref)
#ifdef CONFIG_RT_GROUP_SCHED
/* We've redirected RT tasks to the root task group... */
- ag->tg->rt_se = NULL;
ag->tg->rt_rq = NULL;
#endif
sched_release_group(ag->tg);
@@ -109,7 +108,6 @@ static inline struct autogroup *autogroup_create(void)
* the policy change to proceed.
*/
free_rt_sched_group(tg);
- tg->rt_se = root_task_group.rt_se;
tg->rt_rq = root_task_group.rt_rq;
#endif /* CONFIG_RT_GROUP_SCHED */
tg->autogroup = ag;
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 80e8f4eb3f87..cad8e8a1f519 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -8549,7 +8549,6 @@ static struct kmem_cache *task_group_cache __ro_after_init;
void __init sched_init(void)
{
- unsigned long __maybe_unused ptr = 0;
int i;
/* Make sure the linker didn't screw up */
@@ -8574,14 +8573,7 @@ void __init sched_init(void)
scx_tg_init(&root_task_group);
#endif /* CONFIG_EXT_GROUP_SCHED */
#ifdef CONFIG_RT_GROUP_SCHED
- ptr += 2 * nr_cpu_ids * sizeof(void **);
- ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT);
- root_task_group.rt_se = (struct sched_rt_entity **)ptr;
- ptr += nr_cpu_ids * sizeof(void **);
-
- root_task_group.rt_rq = (struct rt_rq **)ptr;
- ptr += nr_cpu_ids * sizeof(void **);
-
+ root_task_group.rt_rq = &runqueues.rt;
#endif /* CONFIG_RT_GROUP_SCHED */
init_defrootdomain();
diff --git a/kernel/sched/rt.c b/kernel/sched/rt.c
index 0a9b2cd6da72..963004905a7d 100644
--- a/kernel/sched/rt.c
+++ b/kernel/sched/rt.c
@@ -201,26 +201,16 @@ void unregister_rt_sched_group(struct task_group *tg)
if (!rt_group_sched_enabled())
return;
- if (tg->rt_se)
+ if (!is_root_task_group(tg))
destroy_rt_bandwidth(&tg->rt_bandwidth);
}
void free_rt_sched_group(struct task_group *tg)
{
- int i;
-
if (!rt_group_sched_enabled())
return;
- for_each_possible_cpu(i) {
- if (tg->rt_rq)
- kfree(tg->rt_rq[i]);
- if (tg->rt_se)
- kfree(tg->rt_se[i]);
- }
-
- kfree(tg->rt_rq);
- kfree(tg->rt_se);
+ free_percpu(tg->rt_rq);
}
void init_tg_rt_entry(struct task_group *tg, struct rt_rq *rt_rq,
@@ -234,9 +224,6 @@ void init_tg_rt_entry(struct task_group *tg, struct rt_rq *rt_rq,
rt_rq->rq = rq;
rt_rq->tg = tg;
- tg->rt_rq[cpu] = rt_rq;
- tg->rt_se[cpu] = rt_se;
-
if (!rt_se)
return;
@@ -252,42 +239,32 @@ void init_tg_rt_entry(struct task_group *tg, struct rt_rq *rt_rq,
int alloc_rt_sched_group(struct task_group *tg, struct task_group *parent)
{
- struct rt_rq *rt_rq;
+ struct rt_tg_state __percpu *state;
struct sched_rt_entity *rt_se;
+ struct rt_rq *rt_rq;
int i;
if (!rt_group_sched_enabled())
return 1;
- tg->rt_rq = kcalloc(nr_cpu_ids, sizeof(rt_rq), GFP_KERNEL);
- if (!tg->rt_rq)
- goto err;
- tg->rt_se = kcalloc(nr_cpu_ids, sizeof(rt_se), GFP_KERNEL);
- if (!tg->rt_se)
+ state = alloc_percpu_gfp(struct rt_tg_state, GFP_KERNEL);
+ if (!state)
goto err;
+ tg->rt_rq = &state->rt_rq;
init_rt_bandwidth(&tg->rt_bandwidth, ktime_to_ns(global_rt_period()), 0);
for_each_possible_cpu(i) {
- rt_rq = kzalloc_node(sizeof(struct rt_rq),
- GFP_KERNEL, cpu_to_node(i));
- if (!rt_rq)
- goto err;
-
- rt_se = kzalloc_node(sizeof(struct sched_rt_entity),
- GFP_KERNEL, cpu_to_node(i));
- if (!rt_se)
- goto err_free_rq;
+ rt_rq = tg_rt_rq(tg, i);
+ rt_se = rt_rq_se(rt_rq);
init_rt_rq(rt_rq);
rt_rq->rt_runtime = tg->rt_bandwidth.rt_runtime;
- init_tg_rt_entry(tg, rt_rq, rt_se, i, parent->rt_se[i]);
+ init_tg_rt_entry(tg, rt_rq, rt_se, i, tg_rt_se(parent, i));
}
return 1;
-err_free_rq:
- kfree(rt_rq);
err:
return 0;
}
@@ -510,7 +487,7 @@ static inline struct task_group *next_task_group(struct task_group *tg)
#define for_each_rt_rq(rt_rq, iter, rq) \
for (iter = &root_task_group; \
- iter && (rt_rq = iter->rt_rq[cpu_of(rq)]); \
+ iter && (rt_rq = tg_rt_rq(iter, cpu_of(rq))); \
iter = next_task_group(iter))
#define for_each_sched_rt_entity(rt_se) \
@@ -528,11 +505,7 @@ static void sched_rt_rq_enqueue(struct rt_rq *rt_rq)
{
struct task_struct *donor = rq_of_rt_rq(rt_rq)->donor;
struct rq *rq = rq_of_rt_rq(rt_rq);
- struct sched_rt_entity *rt_se;
-
- int cpu = cpu_of(rq);
-
- rt_se = rt_rq->tg->rt_se[cpu];
+ struct sched_rt_entity *rt_se = rt_rq_se(rt_rq);
if (rt_rq->rt_nr_running) {
if (!rt_se)
@@ -547,10 +520,7 @@ static void sched_rt_rq_enqueue(struct rt_rq *rt_rq)
static void sched_rt_rq_dequeue(struct rt_rq *rt_rq)
{
- struct sched_rt_entity *rt_se;
- int cpu = cpu_of(rq_of_rt_rq(rt_rq));
-
- rt_se = rt_rq->tg->rt_se[cpu];
+ struct sched_rt_entity *rt_se = rt_rq_se(rt_rq);
if (!rt_se) {
dequeue_top_rt_rq(rt_rq, rt_rq->rt_nr_running);
@@ -586,7 +556,7 @@ static inline const struct cpumask *sched_rt_period_mask(void)
static inline
struct rt_rq *sched_rt_period_rt_rq(struct rt_bandwidth *rt_b, int cpu)
{
- return container_of(rt_b, struct task_group, rt_bandwidth)->rt_rq[cpu];
+ return tg_rt_rq(container_of(rt_b, struct task_group, rt_bandwidth), cpu);
}
static inline struct rt_bandwidth *sched_rt_bandwidth(struct rt_rq *rt_rq)
@@ -2563,7 +2533,7 @@ static int task_is_throttled_rt(struct task_struct *p, int cpu)
struct rt_rq *rt_rq;
#ifdef CONFIG_RT_GROUP_SCHED // XXX maybe add task_rt_rq(), see also sched_rt_period_rt_rq
- rt_rq = task_group(p)->rt_rq[cpu];
+ rt_rq = tg_rt_rq(task_group(p), cpu);
WARN_ON(!rt_group_sched_enabled() && rt_rq->tg != &root_task_group);
#else
rt_rq = &cpu_rq(cpu)->rt;
@@ -2752,7 +2722,7 @@ static int tg_set_rt_bandwidth(struct task_group *tg,
tg->rt_bandwidth.rt_runtime = rt_runtime;
for_each_possible_cpu(i) {
- struct rt_rq *rt_rq = tg->rt_rq[i];
+ struct rt_rq *rt_rq = tg_rt_rq(tg, i);
raw_spin_lock(&rt_rq->rt_runtime_lock);
rt_rq->rt_runtime = rt_runtime;
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 8ad1780edfe8..62d7c89c74d8 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -488,8 +488,7 @@ struct task_group {
#endif /* CONFIG_FAIR_GROUP_SCHED */
#ifdef CONFIG_RT_GROUP_SCHED
- struct sched_rt_entity **rt_se;
- struct rt_rq **rt_rq;
+ struct rt_rq __percpu *rt_rq;
struct rt_bandwidth rt_bandwidth;
#endif
@@ -2228,6 +2227,41 @@ static inline struct sched_entity *cfs_rq_se(struct cfs_rq *cfs_rq)
}
#endif
+#ifdef CONFIG_RT_GROUP_SCHED
+struct rt_tg_state {
+ struct rt_rq rt_rq;
+ struct sched_rt_entity rt_se;
+} __no_randomize_layout;
+
+/* Access a specific CPU's rt_rq from a task group */
+static inline struct rt_rq *tg_rt_rq(struct task_group *tg, int cpu)
+{
+ return per_cpu_ptr(tg->rt_rq, cpu);
+}
+
+static inline struct sched_rt_entity *tg_rt_se(struct task_group *tg, int cpu)
+{
+ struct rt_tg_state *state;
+
+ if (is_root_task_group(tg))
+ return NULL;
+
+ state = container_of(tg_rt_rq(tg, cpu), struct rt_tg_state, rt_rq);
+ return &state->rt_se;
+}
+
+static inline struct sched_rt_entity *rt_rq_se(struct rt_rq *rt_rq)
+{
+ struct rt_tg_state *state;
+
+ if (is_root_task_group(rt_rq->tg))
+ return NULL;
+
+ state = container_of(rt_rq, struct rt_tg_state, rt_rq);
+ return &state->rt_se;
+}
+#endif /* CONFIG_RT_GROUP_SCHED */
+
/* Change a task's cfs_rq and parent entity if it moves across CPUs/groups */
static inline void set_task_rq(struct task_struct *p, unsigned int cpu)
{
@@ -2250,8 +2284,8 @@ static inline void set_task_rq(struct task_struct *p, unsigned int cpu)
*/
if (!rt_group_sched_enabled())
tg = &root_task_group;
- p->rt.rt_rq = tg->rt_rq[cpu];
- p->rt.parent = tg->rt_se[cpu];
+ p->rt.rt_rq = tg_rt_rq(tg, cpu);
+ p->rt.parent = tg_rt_se(tg, cpu);
#endif /* CONFIG_RT_GROUP_SCHED */
}
--
Thanks and Regards,
Prateek
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v7 3/3] sched/fair: Allocate both cfs_tg_state with percpu allocator
2026-01-19 7:17 ` K Prateek Nayak
@ 2026-01-20 19:11 ` Zecheng Li
0 siblings, 0 replies; 8+ messages in thread
From: Zecheng Li @ 2026-01-20 19:11 UTC (permalink / raw)
To: K Prateek Nayak
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, Rik van Riel, Chris Mason,
Madadi Vineeth Reddy, Xu Liu, Blake Jones, Josh Don, Nilay Vaish,
linux-kernel, Zecheng Li
Hi Prateek,
On Mon, Jan 19, 2026 at 2:17 AM K Prateek Nayak <kprateek.nayak@amd.com> wrote:
>
> Can we also optimize the CONFIG_RT_GROUP_SCHED stuff in the same way
> although I'm assuming the benefit is far less since they aren't
> accessed as frequently as the cfs bits. Something like:
> ...
Thanks for the review. For patch 1 and 2, I'll fix those in v8.
For the RT structs optimization, I agree this would be a nice
improvement for consistency. I'd like to propose handling it as a
follow-up series after this CFS series. I don't currently have a good
test environment for RT workloads to validate performance impact.
Thanks,
Zecheng
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-01-20 19:11 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-01-18 3:34 [PATCH v7 0/3] sched/fair: Optimize cfs_rq and sched_entity allocation for better data locality Zecheng Li
2026-01-18 3:34 ` [PATCH v7 1/3] sched/fair: Co-locate cfs_rq and sched_entity in cfs_tg_state Zecheng Li
2026-01-19 6:40 ` K Prateek Nayak
2026-01-18 3:34 ` [PATCH v7 2/3] sched/fair: Remove task_group->se pointer array Zecheng Li
2026-01-19 6:43 ` K Prateek Nayak
2026-01-18 3:34 ` [PATCH v7 3/3] sched/fair: Allocate both cfs_tg_state with percpu allocator Zecheng Li
2026-01-19 7:17 ` K Prateek Nayak
2026-01-20 19:11 ` Zecheng Li
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Powered by JetHome