Re: [PATCH 0/2] mm/shrinker: per-memcg shrinker units in an xarray, allocated on demand
From: Konstantin Khorenko
Date: Tue Oct 20 2026 - 12:49:01 EST
On 10/19/26 20:32, Harry Yoo wrote:
> On Mon, Oct 19, 2026 at 07:39:22PM +0200, Konstantin Khorenko wrote:
>> Every memory cgroup allocates, per NUMA node, one shrinker_info_unit
>> per 64 memcg-aware shrinker ids up to shrinker_nr_max, whether or not
>> it ever charges an object to those shrinkers, plus a pointer array
>> sized for shrinker_nr_max. Every superblock registers such a shrinker,
>> and shrinker_nr_max never comes down, so a host that mounts many
>> filesystems and runs many cgroups pays for every superblock in every
>> cgroup, live or dying, and keeps paying for new cgroups after the
>> mounts are gone.
>>
>> This replaces patch 2/2 of v2, "mm/shrinker: allocate the per-memcg
>> shrinker units on demand", with the xarray variant discussed in that
>> thread. It applies on top of "mm/shrinker: allocate shrinker_info_unit
>> from a dedicated kmem_cache", patch 1/2 of v2 sent on its own as v3:
>> https://patch.msgid.link/20261019-vstor-144715-shrinker_info_unit-kmem_cache-v3-1-0fb061e09bf4@xxxxxxxxxxxxx
>>
>> Patch 1 keeps the units in an xarray per memcg and node instead of the
>> pointer array. A new unit is inserted where it belongs: nothing is
>> reallocated, copied or published under RCU when shrinker_nr_max grows,
>> and the readers find a unit with xa_load() or walk the present ones
>> with xa_for_each(). The units are still allocated up front, so this
>> patch changes no behaviour.
>>
>> Patch 2 allocates a unit only when the cgroup gets a list on that
>> shrinker's list_lru, which is where map bits are set: a cgroup pays for
>> the shrinkers it holds objects of, not for every superblock on the
>> host. The install is an xarray insert with the gfp of the list
>> allocation; there is no array to race with, so no lock of its own.
>> The root memcg keeps its units for the deferred work of global reclaim
>> and is the only memcg a shrinker registration still visits.
>>
>> Measured on a 4-CPU, single-node VM, 20000 tmpfs mounts (shrinker_nr_max
>> 20096) and 2000 memory cgroups, on three kernels:
>>
>> base v7.3-rc7 with the kmem_cache patch: every cgroup
>> has every unit, in a pointer array
>> xarray base + this series: units on demand, in an xarray
>> old array on demand base + patch 2/2 of v2, rebased: units on demand,
>> in the pointer array
>>
>> old array
>> base xarray on demand
>> slab per empty cgroup 185 KB 18 KB 22 KB
>> shrink_slab_memcg(), empty cgroup 1.6 us 0.2 us 1.6 us
>> same, cgroup with objects on 100 sb 1.9 us 0.9 us 1.7 us
>> mkdir/rmdir the cgroups, 4000 more
>> mounts with them alive same within noise
>
> I think 'old array on demand' is probably good enough, and replacing it
> with xarray is not justifiable unless you have a realistic scenario that
> would benefit from it?
Well, i would not say that i'm happy just to rewrite a lot of code,
too many completely new code, i agree, but i have to admit that
measurements say xarray approach looks better.
For the memory we are kinda good in both cases: we drop most of
redundant memory, the rest numbers are low for both cases.
What the array keeps is three costs that scale with
shrinker_nr_max x cgroups x nodes instead of with what a cgroup uses.
Measured on a 48-CPU, 4-node host (the one from the v1 cover letter)
with 50000 tmpfs mounts (shrinker_nr_max 50112) and 20000 live, empty
memory cgroups:
+--------------------------------------+-----------+-----------+--------+
| | base | old array | xarray |
| | | on demand | |
+--------------------------------------+-----------+-----------+--------+
| unreclaimable slab per empty cgroup | 1.6 MB | 65 KB | 34 KB |
| of it, the per-cgroup container | 32 KB | 32 KB | 0 |
+--------------------------------------+-----------+-----------+--------+
| 640 mounts crossing 10 unit | | | |
| boundaries, 20000 cgroups alive | 1.41 s | 1.31 s | 0.10 s |
| the same 640 mounts, no cgroups | 0.08 s | 0.08 s | 0.07 s |
+--------------------------------------+-----------+-----------+--------+
| echo 2 > drop_caches: shrink_slab() | | | |
| for every online cgroup, the walk | | | |
| kswapd does on every pass | 23 s | 11.3 s | 1.3 s |
| of it, shrink_slab_memcg() per | | | |
| cgroup and node | 22.8 us | 10.7 us | 0.5 us |
+--------------------------------------+-----------+-----------+--------+
base is v7.3-rc7 with the kmem_cache patch: every cgroup gets every
unit; both on-demand variants sit on top of it.
The array row by row:
- The container is a pointer array sized for shrinker_nr_max, 6 KB
here rounded up to kmalloc-8k, per cgroup and node, for cgroups that
hold nothing: 32 KB per cgroup on four nodes, 640 MB for the 20000.
- A registration that crosses a unit boundary reallocates and copies
that array for every cgroup on every node under shrinker_mutex, which
every other mount and umount then waits for: about 120 ms per
boundary here.
- shrink_slab_memcg() walks shrinker_nr_max / 64 indexes per cgroup
and node on every reclaim pass, whether the cgroup has objects or
not. The 11 s of drop_caches here is that walk; kswapd pays it at
every priority.
The xarray version pays for the units a cgroup has and nothing per
shrinker_nr_max, so none of the three rows grows with the number of
mounts.
Surprising it is also less code at its final version: it removes the
array reallocation path and the spinlock the on-demand array needs so
that an install is not lost in a copy, and ends up with fewer lines than
before the series.
--
Cheers,
Konstantin.