[PATCH v5] sched: reorder some fields in struct rq

Blake Jones posted 1 patch 1 week, 5 days ago
kernel/sched/sched.h | 80 +++++++++++++++++++++++++++-----------------
1 file changed, 50 insertions(+), 30 deletions(-)
[PATCH v5] sched: reorder some fields in struct rq
Posted by Blake Jones 1 week, 5 days ago
This colocates some hot fields in "struct rq" to be on the same cache line
as others that are often accessed at the same time or in similar ways.

Using data from a Google-internal fleet-scale profiler, I found three
distinct groups of hot fields in struct rq:

- (1) The runqueue lock: __lock.

- (2) Those accessed from hot code in pick_next_task_fair():
      nr_running, nr_numa_running, nr_preferred_running,
      ttwu_pending, cpu_capacity, curr, idle.

- (3) Those accessed from some other hot codepaths, e.g.
      update_curr(), update_rq_clock(), and scheduler_tick():
      clock_task, clock_pelt, clock, lost_idle_time,
      clock_update_flags, clock_pelt_idle, clock_idle.

The cycles spent on accessing these different groups of fields broke down
roughly as follows:

- 50% on group (1) (the runqueue lock, always read-write)
- 39% on group (2) (load:store ratio around 38:1)
-  8% on group (3) (load:store ratio around 5:1)
-  3% on all the other fields

Most of the fields in group (3) are already in a cache line grouping; this
patch just adds "clock" and "clock_update_flags" to that group. The fields
in group (2) are scattered across several cache lines; the main effect of
this patch is to group them together, on a single line at the beginning of
the structure. A few other less performance-critical fields (nr_switches,
numa_migrate_on, has_blocked_load, nohz_csd, last_blocked_load_update_tick)
were also reordered to reduce holes in the data structure.

Since the runqueue lock is acquired from so many different contexts, and is
basically always accessed using an atomic operation, putting it on either
of the cache lines for groups (2) or (3) would slow down accesses to those
fields dramatically, since those groups are read-mostly accesses.

To test this, I wrote a focused load test that would put load on the
pick_next_task_fair() path. A parent process would fork many child
processes, and each child would nanosleep() for 1 msec many times in a
loop. The load test was monitored with "perf", and I looked at the amount
of cycles that were spent with sched_balance_rq() on the stack. The test
was reliably spending ~5% of all of its cycles there. I ran it 100 times
on a pair of 2-socket Intel Haswell machines (72 vCPUs per machine) - one
running the tip of sched/core, the other running this change - using 360
child processes and 8192 1-msec sleeps per child.  The mean cycle count
dropped from 5.14B to 4.91B, or a *4.6% decrease* in relevant scheduler
cycles.

Given that this change reduces cache misses in a very hot kernel codepath,
there's likely to be additional application performance improvement due to
reduced cache conflicts from kernel data structures.

On a Power11 system with 128-byte cache lines, my test showed a ~5%
decrease in relevant scheduler cycles, along with a slight increase in user
time - both positive indicators. This data comes from
https://lore.kernel.org/lkml/affdc6b1-9980-44d1-89db-d90730c1e384@linux.ibm.com/
This is the case even though the additional "____cacheline_aligned" that
puts the runqueue lock on the next cache line adds an additional 64 bytes
of padding on those machines. This patch does not change the size of
"struct rq" on machines with 64-byte cache lines.

I also ran "hackbench" to try to test this change, but it didn't show
conclusive results.  Looking at a CPU cycle profile of the hackbench run,
it was spending 95% of its cycles inside __alloc_skb(), __kfree_skb(), or
kmem_cache_free() - almost all of which was spent updating memcg counters
or contending on the list_lock in kmem_cache_node. And it spent less than
0.5% of its cycles inside either schedule() or try_to_wake_up().  So it's
not surprising that it didn't show useful results here.

The "__no_randomize_layout" was added to reflect the fact that performance
of code that references this data structure is unusually sensitive to
placement of its members.

Signed-off-by: Blake Jones <blakejones@google.com>
Reviewed-by: Madadi Vineeth Reddy <vineethr@linux.ibm.com>
Tested-by: Madadi Vineeth Reddy <vineethr@linux.ibm.com>
---
 kernel/sched/sched.h | 80 +++++++++++++++++++++++++++-----------------
 1 file changed, 50 insertions(+), 30 deletions(-)

v4 -> v5 changes:
- Resynced with sched/core.
  Link to v4: https://lore.kernel.org/lkml/20251029185458.3040228-1-blakejones@google.com/T/#u

v3 -> v4 changes:
- Updated based on review comments from peterz@infradead.org:
  updated block comment in struct rq, moved "nr_iowait" to the end of the
  structure, and added "__no_randomize_layout" tag to the structure.
  Link to v3: https://lore.kernel.org/lkml/20251028080348.2177469-1-blakejones@google.com/T/#u

v2 -> v3 changes:
- Resynced and retested. Results are consistent with v2.
  Link to v2: https://lore.kernel.org/lkml/20250730205644.2595052-1-blakejones@google.com/T/#u

v1 -> v2 changes:
- Resynced to tip/sched/core tree, which has removed CONFIG_SMP.
  Link to v1: https://lore.kernel.org/lkml/20250722201339.1198239-1-blakejones@google.com/T/#u

v1 was sent on July 22 of this year, over four months ago.

diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index b419a4d98461..6d87cc1d6f7f 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -1115,28 +1115,50 @@ DECLARE_STATIC_KEY_FALSE(sched_uclamp_used);
  * acquire operations must be ordered by ascending &runqueue.
  */
 struct rq {
-	/* runqueue lock: */
-	raw_spinlock_t		__lock;
-
-	/* Per class runqueue modification mask; bits in class order. */
-	unsigned int		queue_mask;
+	/*
+	 * The following members are loaded together, without holding the
+	 * rq->lock, in an extremely hot loop in update_sg_lb_stats()
+	 * (called from pick_next_task()). To reduce cache pollution from
+	 * this operation, they are placed together on this dedicated cache
+	 * line. Even though some of them are frequently modified, they are
+	 * loaded much more frequently than they are stored.
+	 */
 	unsigned int		nr_running;
 #ifdef CONFIG_NUMA_BALANCING
 	unsigned int		nr_numa_running;
 	unsigned int		nr_preferred_running;
-	unsigned int		numa_migrate_on;
 #endif
+	unsigned int		ttwu_pending;
+	unsigned long		cpu_capacity;
+#ifdef CONFIG_SCHED_PROXY_EXEC
+	struct task_struct __rcu	*donor;  /* Scheduling context */
+	struct task_struct __rcu	*curr;   /* Execution context */
+#else
+	union {
+		struct task_struct __rcu *donor; /* Scheduler context */
+		struct task_struct __rcu *curr;  /* Execution context */
+	};
+#endif
+	struct task_struct	*idle;
+	/* padding left here deliberately */
+
+	/*
+	 * The next cacheline holds the (hot) runqueue lock, as well as
+	 * some other less performance-critical fields.
+	 */
+	u64			nr_switches	____cacheline_aligned;
+
+	/* runqueue lock: */
+	raw_spinlock_t		__lock;
+
 #ifdef CONFIG_NO_HZ_COMMON
-	unsigned long		last_blocked_load_update_tick;
-	unsigned int		has_blocked_load;
-	call_single_data_t	nohz_csd;
 	unsigned int		nohz_tick_stopped;
 	atomic_t		nohz_flags;
+	unsigned int		has_blocked_load;
+	unsigned long		last_blocked_load_update_tick;
+	call_single_data_t	nohz_csd;
 #endif /* CONFIG_NO_HZ_COMMON */
 
-	unsigned int		ttwu_pending;
-	u64			nr_switches;
-
 #ifdef CONFIG_UCLAMP_TASK
 	/* Utilization clamp values based on CPU's RUNNABLE tasks */
 	struct uclamp_rq	uclamp[UCLAMP_CNT] ____cacheline_aligned;
@@ -1159,6 +1181,9 @@ struct rq {
 	struct list_head	*tmp_alone_branch;
 #endif /* CONFIG_FAIR_GROUP_SCHED */
 
+#ifdef CONFIG_NUMA_BALANCING
+	unsigned int		numa_migrate_on;
+#endif
 	/*
 	 * This is part of a global counter where only the total sum
 	 * over all CPUs matters. A task can increase this counter on
@@ -1167,36 +1192,31 @@ struct rq {
 	 */
 	unsigned long 		nr_uninterruptible;
 
-#ifdef CONFIG_SCHED_PROXY_EXEC
-	struct task_struct __rcu	*donor;  /* Scheduling context */
-	struct task_struct __rcu	*curr;   /* Execution context */
-#else
-	union {
-		struct task_struct __rcu *donor; /* Scheduler context */
-		struct task_struct __rcu *curr;  /* Execution context */
-	};
-#endif
 	struct sched_dl_entity	*dl_server;
-	struct task_struct	*idle;
 	struct task_struct	*stop;
 	unsigned long		next_balance;
 	struct mm_struct	*prev_mm;
 
-	unsigned int		clock_update_flags;
-	u64			clock;
-	/* Ensure that all clocks are in the same cache line */
+	/* Per class runqueue modification mask; bits in class order. */
+	unsigned int		queue_mask;
+
+	/*
+	 * The following fields of clock data are frequently referenced
+	 * and updated together, and should go on their own cache line.
+	 */
 	u64			clock_task ____cacheline_aligned;
 	u64			clock_pelt;
+	u64			clock;
 	unsigned long		lost_idle_time;
+	unsigned int		clock_update_flags;
 	u64			clock_pelt_idle;
 	u64			clock_idle;
+
 #ifndef CONFIG_64BIT
 	u64			clock_pelt_idle_copy;
 	u64			clock_idle_copy;
 #endif
 
-	atomic_t		nr_iowait;
-
 	u64 last_seen_need_resched_ns;
 	int ticks_without_resched;
 
@@ -1207,8 +1227,6 @@ struct rq {
 	struct root_domain		*rd;
 	struct sched_domain __rcu	*sd;
 
-	unsigned long		cpu_capacity;
-
 	struct balance_callback *balance_callback;
 
 	unsigned char		nohz_idle_balance;
@@ -1318,7 +1336,9 @@ struct rq {
 	call_single_data_t	cfsb_csd;
 	struct list_head	cfsb_csd_list;
 #endif
-};
+
+	atomic_t		nr_iowait;
+} __no_randomize_layout;
 
 #ifdef CONFIG_FAIR_GROUP_SCHED
 
-- 
2.52.0.177.g9f829587af-goog
Re: [PATCH v5] sched: reorder some fields in struct rq
Posted by Josh Don 1 week, 2 days ago
Hi Blake,

On Mon, Dec 1, 2025 at 6:37 PM Blake Jones <blakejones@google.com> wrote:
>
> This colocates some hot fields in "struct rq" to be on the same cache line
> as others that are often accessed at the same time or in similar ways.
[snip]
> Signed-off-by: Blake Jones <blakejones@google.com>
> Reviewed-by: Madadi Vineeth Reddy <vineethr@linux.ibm.com>
> Tested-by: Madadi Vineeth Reddy <vineethr@linux.ibm.com>

Sorry, I thought I had already included a review tag when I responded
to the earlier version of this.

Reviewed-by: Josh Don <joshdon@google.com>

> ---
>  kernel/sched/sched.h | 80 +++++++++++++++++++++++++++-----------------
>  1 file changed, 50 insertions(+), 30 deletions(-)
>
> v4 -> v5 changes:
> - Resynced with sched/core.
>   Link to v4: https://lore.kernel.org/lkml/20251029185458.3040228-1-blakejones@google.com/T/#u
>
> v3 -> v4 changes:
> - Updated based on review comments from peterz@infradead.org:
>   updated block comment in struct rq, moved "nr_iowait" to the end of the
>   structure, and added "__no_randomize_layout" tag to the structure.
>   Link to v3: https://lore.kernel.org/lkml/20251028080348.2177469-1-blakejones@google.com/T/#u
>
> v2 -> v3 changes:
> - Resynced and retested. Results are consistent with v2.
>   Link to v2: https://lore.kernel.org/lkml/20250730205644.2595052-1-blakejones@google.com/T/#u
>
> v1 -> v2 changes:
> - Resynced to tip/sched/core tree, which has removed CONFIG_SMP.
>   Link to v1: https://lore.kernel.org/lkml/20250722201339.1198239-1-blakejones@google.com/T/#u
>
> v1 was sent on July 22 of this year, over four months ago.

The patch is straightforward and the results look compelling;
Peter/Ingo, any thoughts?

Best,
Josh
Re: [PATCH v5] sched: reorder some fields in struct rq
Posted by Peter Zijlstra 1 week, 1 day ago
On Thu, Dec 04, 2025 at 02:56:47PM -0800, Josh Don wrote:
> Hi Blake,
> 
> On Mon, Dec 1, 2025 at 6:37 PM Blake Jones <blakejones@google.com> wrote:
> >
> > This colocates some hot fields in "struct rq" to be on the same cache line
> > as others that are often accessed at the same time or in similar ways.
> [snip]
> > Signed-off-by: Blake Jones <blakejones@google.com>
> > Reviewed-by: Madadi Vineeth Reddy <vineethr@linux.ibm.com>
> > Tested-by: Madadi Vineeth Reddy <vineethr@linux.ibm.com>
> 
> Sorry, I thought I had already included a review tag when I responded
> to the earlier version of this.
> 
> Reviewed-by: Josh Don <joshdon@google.com>
> 
> > ---
> >  kernel/sched/sched.h | 80 +++++++++++++++++++++++++++-----------------
> >  1 file changed, 50 insertions(+), 30 deletions(-)
> >
> > v4 -> v5 changes:
> > - Resynced with sched/core.
> >   Link to v4: https://lore.kernel.org/lkml/20251029185458.3040228-1-blakejones@google.com/T/#u
> >
> > v3 -> v4 changes:
> > - Updated based on review comments from peterz@infradead.org:
> >   updated block comment in struct rq, moved "nr_iowait" to the end of the
> >   structure, and added "__no_randomize_layout" tag to the structure.
> >   Link to v3: https://lore.kernel.org/lkml/20251028080348.2177469-1-blakejones@google.com/T/#u
> >
> > v2 -> v3 changes:
> > - Resynced and retested. Results are consistent with v2.
> >   Link to v2: https://lore.kernel.org/lkml/20250730205644.2595052-1-blakejones@google.com/T/#u
> >
> > v1 -> v2 changes:
> > - Resynced to tip/sched/core tree, which has removed CONFIG_SMP.
> >   Link to v1: https://lore.kernel.org/lkml/20250722201339.1198239-1-blakejones@google.com/T/#u
> >
> > v1 was sent on July 22 of this year, over four months ago.
> 
> The patch is straightforward and the results look compelling;
> Peter/Ingo, any thoughts?

I've stuck it on the pile for post -rc1 time.
Re: [PATCH v5] sched: reorder some fields in struct rq
Posted by Blake Jones 1 week, 1 day ago
On Fri, Dec 5, 2025 at 2:49 AM Peter Zijlstra <peterz@infradead.org> wrote:
> I've stuck it on the pile for post -rc1 time.

Thanks so much, Peter!