Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v1] mm/memcontrol: drop non-hierarchical memcg-wide stats in non-v1 kernels
@ 2026-09-01 23:28 Joanne Koong
  2026-09-01 23:42 ` Yosry Ahmed
  0 siblings, 1 reply; 3+ messages in thread
From: Joanne Koong @ 2026-09-01 23:28 UTC (permalink / raw)
  To: akpm, hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song
  Cc: yosry, linux-mm, cgroups

memcg_vmstats keeps a non-hierarchical copy of every memcg-wide stat item
and event alongside the hierarchical one. The only readers however are
the legacy memory.stat and memory.numa_stat, and reparenting on offline.
All of them live under CONFIG_MEMCG_V1, and their accessors
(memcg_page_state_local() and memcg_events_local()) are already compiled
out with it. This means on a CONFIG_MEMCG_V1=n kernel, memcg_vmstats's
non-hierarchial arrays are written to on every rstat flush, despite
their values never being read / accessed.

Compile this out and only carry the non-hierarchical memcg-wide stat
arrays if the kernel actually supports v1.

This makes flushes cheaper. mem_cgroup_stat_aggregate() can now skip the
read-modify-write of ac->local[i]. Nothing else on a kernel with
CONFIG_MEMCG_V1=n accesses state_local or events_local, so those
cachelines get pulled in solely for the writes, and they are separate
from the ones the loop is already walking / accessing.

On a 80-cpu x86_64 machine with 500 cgroups each running a workload that
dirties anon, file, dirty/writeback, slab, kmem, mlock, and reclaim
counters, timing mem_cgroup_css_rstat_flush() in-kernel in TSC ticks per
flush showed roughly

                         before   after    delta
memcg-wide aggregation   1231     1180     -4.1%
overall flush function   2452     2397     -2.2%

These numbers are from taking the median of 70 samples, one per 20s
window on each kernel. The 95% intervals observed on the two deltas are
[-5.41%, -1.76%] and [-4.53%, -0.14%]. The values above include the
timing overhead itself, so only the delta is meaningful here.

Counting the items that actually changed, a median of 1.5 of the 77
memcg-wide items (57 state + 20 events) had a non-zero per-cpu delta at
each flush, which means the benchmarks above are with one or two fewer
cachelines pulled in per flush. The count is low because the benchmark
reads memory.stat in a loop to keep the flush rate up. For cases where
flushes are triggered only by the 2s periodic worker, more changes will
have accumulated between flushes, so more cachelines are skipped and the
per-flush saving should be larger.

Please note that the per-node lruvec array is left unchanged, as that is
not v1-only (count_shadow_nodes() needs to read
lruvec_page_state_local() under CONFIG_MEMCG rather than
CONFIG_MEMCG_V1).

This adds no functional changes to kernel builds with CONFIG_MEMCG_V1=y.

Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 mm/memcontrol.c | 49 ++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 42 insertions(+), 7 deletions(-)

diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 7ce50bccf126..b98b2d68c62b 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -675,9 +675,11 @@ struct memcg_vmstats {
 	long			state[MEMCG_VMSTAT_SIZE];
 	unsigned long		events[NR_MEMCG_EVENTS];
 
+#ifdef CONFIG_MEMCG_V1
 	/* Non-hierarchical (CPU aggregated) page state & events */
 	long			state_local[MEMCG_VMSTAT_SIZE];
 	unsigned long		events_local[NR_MEMCG_EVENTS];
+#endif
 
 	/* Pending child counts during tree propagation */
 	long			state_pending[MEMCG_VMSTAT_SIZE];
@@ -687,6 +689,32 @@ struct memcg_vmstats {
 	atomic_long_t		stats_updates;
 };
 
+/*
+ * The non-hierarchical memcg-wide counters are read back only by the legacy
+ * memory.stat and by reparenting on offline, both of which are v1-only.
+ */
+#ifdef CONFIG_MEMCG_V1
+static long *memcg_state_local_array(struct mem_cgroup *memcg)
+{
+	return memcg->vmstats->state_local;
+}
+
+static unsigned long *memcg_events_local_array(struct mem_cgroup *memcg)
+{
+	return memcg->vmstats->events_local;
+}
+#else
+static long *memcg_state_local_array(struct mem_cgroup *memcg)
+{
+	return NULL;
+}
+
+static unsigned long *memcg_events_local_array(struct mem_cgroup *memcg)
+{
+	return NULL;
+}
+#endif
+
 /*
  * memcg and lruvec stats flushing
  *
@@ -4468,7 +4496,10 @@ static void mem_cgroup_css_reset(struct cgroup_subsys_state *css)
 struct aggregate_control {
 	/* pointer to the aggregated (CPU and subtree aggregated) counters */
 	long *aggregate;
-	/* pointer to the non-hierarchichal (CPU aggregated) counters */
+	/*
+	 * pointer to the non-hierarchical (CPU aggregated) counters or NULL to
+	 * skip updating them (see memcg_state_local_array())
+	 */
 	long *local;
 	/* pointer to the pending child counters during tree propagation */
 	long *pending;
@@ -4507,7 +4538,7 @@ static void mem_cgroup_stat_aggregate(struct aggregate_control *ac)
 		}
 
 		/* Aggregate counts on this level and propagate upwards */
-		if (delta_cpu)
+		if (delta_cpu && ac->local)
 			ac->local[i] += delta_cpu;
 
 		if (delta) {
@@ -4521,6 +4552,7 @@ static void mem_cgroup_stat_aggregate(struct aggregate_control *ac)
 #ifdef CONFIG_MEMCG_NMI_SAFETY_REQUIRES_ATOMIC
 static void flush_nmi_stats(struct mem_cgroup *memcg, struct mem_cgroup *parent)
 {
+	long *state_local = memcg_state_local_array(memcg);
 	int nid;
 
 	if (atomic_read(&memcg->kmem_stat)) {
@@ -4528,7 +4560,8 @@ static void flush_nmi_stats(struct mem_cgroup *memcg, struct mem_cgroup *parent)
 		int index = memcg_stats_index(MEMCG_KMEM);
 
 		memcg->vmstats->state[index] += kmem;
-		memcg->vmstats->state_local[index] += kmem;
+		if (state_local)
+			state_local[index] += kmem;
 		if (parent)
 			parent->vmstats->state_pending[index] += kmem;
 	}
@@ -4550,7 +4583,8 @@ static void flush_nmi_stats(struct mem_cgroup *memcg, struct mem_cgroup *parent)
 			if (plstats)
 				plstats->state_pending[index] += slab;
 			memcg->vmstats->state[index] += slab;
-			memcg->vmstats->state_local[index] += slab;
+			if (state_local)
+				state_local[index] += slab;
 			if (parent)
 				parent->vmstats->state_pending[index] += slab;
 		}
@@ -4563,7 +4597,8 @@ static void flush_nmi_stats(struct mem_cgroup *memcg, struct mem_cgroup *parent)
 			if (plstats)
 				plstats->state_pending[index] += slab;
 			memcg->vmstats->state[index] += slab;
-			memcg->vmstats->state_local[index] += slab;
+			if (state_local)
+				state_local[index] += slab;
 			if (parent)
 				parent->vmstats->state_pending[index] += slab;
 		}
@@ -4588,7 +4623,7 @@ static void mem_cgroup_css_rstat_flush(struct cgroup_subsys_state *css, int cpu)
 
 	ac = (struct aggregate_control) {
 		.aggregate = memcg->vmstats->state,
-		.local = memcg->vmstats->state_local,
+		.local = memcg_state_local_array(memcg),
 		.pending = memcg->vmstats->state_pending,
 		.ppending = parent ? parent->vmstats->state_pending : NULL,
 		.cstat = statc->state,
@@ -4599,7 +4634,7 @@ static void mem_cgroup_css_rstat_flush(struct cgroup_subsys_state *css, int cpu)
 
 	ac = (struct aggregate_control) {
 		.aggregate = memcg->vmstats->events,
-		.local = memcg->vmstats->events_local,
+		.local = memcg_events_local_array(memcg),
 		.pending = memcg->vmstats->events_pending,
 		.ppending = parent ? parent->vmstats->events_pending : NULL,
 		.cstat = statc->events,
-- 
2.52.0



^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH v1] mm/memcontrol: drop non-hierarchical memcg-wide stats in non-v1 kernels
  2026-09-01 23:28 [PATCH v1] mm/memcontrol: drop non-hierarchical memcg-wide stats in non-v1 kernels Joanne Koong
@ 2026-09-01 23:42 ` Yosry Ahmed
  2026-09-02  5:04   ` Joanne Koong
  0 siblings, 1 reply; 3+ messages in thread
From: Yosry Ahmed @ 2026-09-01 23:42 UTC (permalink / raw)
  To: Joanne Koong
  Cc: akpm, hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
	linux-mm, cgroups

On Tue, Sep 1, 2026 at 4:32 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> memcg_vmstats keeps a non-hierarchical copy of every memcg-wide stat item
> and event alongside the hierarchical one. The only readers however are
> the legacy memory.stat and memory.numa_stat, and reparenting on offline.
> All of them live under CONFIG_MEMCG_V1, and their accessors
> (memcg_page_state_local() and memcg_events_local()) are already compiled
> out with it. This means on a CONFIG_MEMCG_V1=n kernel, memcg_vmstats's
> non-hierarchial arrays are written to on every rstat flush, despite
> their values never being read / accessed.
>
> Compile this out and only carry the non-hierarchical memcg-wide stat
> arrays if the kernel actually supports v1.
>
> This makes flushes cheaper. mem_cgroup_stat_aggregate() can now skip the
> read-modify-write of ac->local[i]. Nothing else on a kernel with
> CONFIG_MEMCG_V1=n accesses state_local or events_local, so those
> cachelines get pulled in solely for the writes, and they are separate
> from the ones the loop is already walking / accessing.
>
> On a 80-cpu x86_64 machine with 500 cgroups each running a workload that
> dirties anon, file, dirty/writeback, slab, kmem, mlock, and reclaim
> counters, timing mem_cgroup_css_rstat_flush() in-kernel in TSC ticks per
> flush showed roughly
>
>                          before   after    delta
> memcg-wide aggregation   1231     1180     -4.1%
> overall flush function   2452     2397     -2.2%
>
> These numbers are from taking the median of 70 samples, one per 20s
> window on each kernel. The 95% intervals observed on the two deltas are
> [-5.41%, -1.76%] and [-4.53%, -0.14%]. The values above include the
> timing overhead itself, so only the delta is meaningful here.
>
> Counting the items that actually changed, a median of 1.5 of the 77
> memcg-wide items (57 state + 20 events) had a non-zero per-cpu delta at
> each flush, which means the benchmarks above are with one or two fewer
> cachelines pulled in per flush. The count is low because the benchmark
> reads memory.stat in a loop to keep the flush rate up. For cases where
> flushes are triggered only by the 2s periodic worker, more changes will
> have accumulated between flushes, so more cachelines are skipped and the
> per-flush saving should be larger.
>
> Please note that the per-node lruvec array is left unchanged, as that is
> not v1-only (count_shadow_nodes() needs to read
> lruvec_page_state_local() under CONFIG_MEMCG rather than
> CONFIG_MEMCG_V1).
>
> This adds no functional changes to kernel builds with CONFIG_MEMCG_V1=y.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
>  mm/memcontrol.c | 49 ++++++++++++++++++++++++++++++++++++++++++-------
>  1 file changed, 42 insertions(+), 7 deletions(-)
>
> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
> index 7ce50bccf126..b98b2d68c62b 100644
> --- a/mm/memcontrol.c
> +++ b/mm/memcontrol.c
> @@ -675,9 +675,11 @@ struct memcg_vmstats {
>         long                    state[MEMCG_VMSTAT_SIZE];
>         unsigned long           events[NR_MEMCG_EVENTS];
>
> +#ifdef CONFIG_MEMCG_V1
>         /* Non-hierarchical (CPU aggregated) page state & events */
>         long                    state_local[MEMCG_VMSTAT_SIZE];
>         unsigned long           events_local[NR_MEMCG_EVENTS];
> +#endif
>
>         /* Pending child counts during tree propagation */
>         long                    state_pending[MEMCG_VMSTAT_SIZE];
> @@ -687,6 +689,32 @@ struct memcg_vmstats {
>         atomic_long_t           stats_updates;
>  };
>
> +/*
> + * The non-hierarchical memcg-wide counters are read back only by the legacy
> + * memory.stat and by reparenting on offline, both of which are v1-only.
> + */
> +#ifdef CONFIG_MEMCG_V1

Can we query the boot parameter instead of the config option here?
IIUC this should work even if v1 is only disabled at boot.


^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH v1] mm/memcontrol: drop non-hierarchical memcg-wide stats in non-v1 kernels
  2026-09-01 23:42 ` Yosry Ahmed
@ 2026-09-02  5:04   ` Joanne Koong
  0 siblings, 0 replies; 3+ messages in thread
From: Joanne Koong @ 2026-09-02  5:04 UTC (permalink / raw)
  To: Yosry Ahmed
  Cc: akpm, hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
	linux-mm, cgroups

On Tue, Sep 1, 2026 at 4:42 PM Yosry Ahmed <yosry@kernel.org> wrote:
>
> > +/*
> > + * The non-hierarchical memcg-wide counters are read back only by the legacy
> > + * memory.stat and by reparenting on offline, both of which are v1-only.
> > + */
> > +#ifdef CONFIG_MEMCG_V1
>
> Can we query the boot parameter instead of the config option here?
> IIUC this should work even if v1 is only disabled at boot.

This seems like a good idea to me. Maybe we should take it one step
further and gate it behind cgroup_subsys_on_dfl() to cover any
CONFIG_MEMCG_V1=y kernel that doesn't use/mount v1? Not a memcg expert
but afaict, if the kernel then switches from default (v2) to legacy
(v1), we could populate the state_local and events_local arrays from
the aggregated state / events arrays in a .bind callback since
switching from v2 to v1 is only allowed if there's no memcg subtrees.

Thanks,
Joanne


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-09-02  5:04 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-01 23:28 [PATCH v1] mm/memcontrol: drop non-hierarchical memcg-wide stats in non-v1 kernels Joanne Koong
2026-09-01 23:42 ` Yosry Ahmed
2026-09-02  5:04   ` Joanne Koong

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox