Rotating or onlining a memcg currently uses hlist_nulls_add_tail_rcu()
which walks the destination bin to find its tail while holding
`memcg_lru.lock` with interrupts disabled.  With thousands of cgroups,
this takes a long time and causes severe lock contention.

Cache a pointer to each bin's last node, making tail insertion O(1).
This requires remembering the bin number (but the new struct field
occupies a hole, so it does not grow).

In a micro benchmark that creates 1500 cgroups and repeatedly writes
`1024M swappiness=0` to `memory.reclaim` in 8 threads, the request
rate increases by a factor of 9.5 and the average duration of each
write request is reduced by 89%.

All writes return EAGAIN in both runs; the benchmark measures the cost
of reclaim attempts that cannot meet their requested target, not
reclaimed throughput.

Signed-off-by: Max Kellermann <[email protected]>
---
 include/linux/mmzone.h |  4 ++++
 mm/vmscan.c            | 24 +++++++++++++++++++++---
 2 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index 94f9c3ff5416..9c1c1f153d1f 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -586,6 +586,8 @@ struct lru_gen_folio {
        u8 gen;
        /* the list segment this lru_gen_folio belongs to */
        u8 seg;
+       /* the memcg bin, protected by memcg_lru.lock */
+       u8 bin;
        /* per-node lru_gen_folio list for global reclaim */
        struct hlist_nulls_node list;
 };
@@ -683,6 +685,8 @@ struct lru_gen_memcg {
        unsigned long nr_memcgs[MEMCG_NR_GENS];
        /* per-node lru_gen_folio list for global reclaim */
        struct hlist_nulls_head fifo[MEMCG_NR_GENS][MEMCG_NR_BINS];
+       /* last entry in each bin, or NULL if empty */
+       struct hlist_nulls_node *tail[MEMCG_NR_GENS][MEMCG_NR_BINS];
        /* protects the above */
        spinlock_t lock;
 };
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 3d4bc12cf18e..69976df5d537 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -4384,21 +4384,37 @@ static void lru_gen_add_memcg(struct lru_gen_memcg 
*memcg_lru,
                              int gen, int bin, bool at_head)
 {
        struct hlist_nulls_head *head = &memcg_lru->fifo[gen][bin];
+       struct hlist_nulls_node *last = memcg_lru->tail[gen][bin];
 
        lockdep_assert_held(&memcg_lru->lock);
 
-       if (at_head)
+       lrugen->bin = bin;
+
+       if (at_head || !last)
                hlist_nulls_add_head_rcu(&lrugen->list, head);
        else
-               hlist_nulls_add_tail_rcu(&lrugen->list, head);
+               hlist_nulls_add_behind_rcu(&lrugen->list, last);
+
+       if (!at_head || !last)
+               memcg_lru->tail[gen][bin] = &lrugen->list;
 }
 
 static void lru_gen_del_memcg(struct lru_gen_memcg *memcg_lru,
                              struct lru_gen_folio *lrugen,
                              int gen)
 {
+       const int bin = lrugen->bin;
+       struct hlist_nulls_head *head = &memcg_lru->fifo[gen][bin];
+
        lockdep_assert_held(&memcg_lru->lock);
 
+       /* pprev points to head->first for the only entry, otherwise 
prev->next. */
+       if (memcg_lru->tail[gen][bin] == &lrugen->list)
+               memcg_lru->tail[gen][bin] = lrugen->list.pprev == &head->first
+                       ? NULL
+                       : container_of(lrugen->list.pprev, struct 
hlist_nulls_node, next);
+
+       /* Keep next intact for readers that still hold this entry. */
        hlist_nulls_del_init_rcu(&lrugen->list);
 }
 
@@ -5883,8 +5899,10 @@ void lru_gen_init_pgdat(struct pglist_data *pgdat)
        spin_lock_init(&pgdat->memcg_lru.lock);
 
        for (i = 0; i < MEMCG_NR_GENS; i++) {
-               for (j = 0; j < MEMCG_NR_BINS; j++)
+               for (j = 0; j < MEMCG_NR_BINS; j++) {
                        INIT_HLIST_NULLS_HEAD(&pgdat->memcg_lru.fifo[i][j], i);
+                       pgdat->memcg_lru.tail[i][j] = NULL;
+               }
        }
 }
 
-- 
2.47.3


Reply via email to