* [PATCH 1/6] mm/mglru: batch update lrugen->nr_pages in inc_min_seq()
2026-08-21 10:25 [PATCH 0/6] mm/mglru: speed up inc_min_seq() and fix cold/hot inversions Barry Song (Xiaomi)
@ 2026-08-21 10:25 ` Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 2/6] mm/mglru: batch update lrugen->protected " Barry Song (Xiaomi)
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Barry Song (Xiaomi) @ 2026-08-21 10:25 UTC (permalink / raw)
To: akpm, linux-mm
Cc: axelrasmussen, baolin.wang, baoquan.he, chenridong, david, hannes,
kasong, lianux.mm, linux-kernel, ljs, lyugaofei, mhocko, qi.zheng,
shakeel.butt, stevensd, wangzicheng, weixugc, yuanchu, zhangbo56,
Barry Song (Xiaomi)
Currently, folio_inc_gen() updates lrugen->nr_pages for every folio
as it advances generations. Instead, accumulate the size changes
and update lrugen->nr_pages in a batch after scanning the entire
oldest generation, or when the scan stops because remaining reaches
zero.
Since we only move folios from the oldest generation to the second
oldest generation, the active/inactive state cannot change. We can
therefore skip __lru_update_size().
Signed-off-by: Barry Song (Xiaomi) <baohua@kernel.org>
---
mm/vmscan.c | 46 +++++++++++++++++++++++++++++++++++-----------
1 file changed, 35 insertions(+), 11 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index c1404a59523d..0d74fc00abd3 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3296,20 +3296,21 @@ static int folio_update_gen(struct folio *folio, int gen, const vma_flags_t *vma
}
/* protect pages accessed multiple times through file descriptors */
-static int folio_inc_gen(struct lruvec *lruvec, struct folio *folio)
+static int __folio_inc_gen(struct folio *folio, int old_gen, bool *increased)
{
- int type = folio_is_file_lru(folio);
- struct lru_gen_folio *lrugen = &lruvec->lrugen;
- int new_gen, old_gen = lru_gen_from_seq(lrugen->min_seq[type]);
unsigned long new_flags, old_flags = READ_ONCE(folio->flags.f);
+ int new_gen;
VM_WARN_ON_ONCE_FOLIO(!(old_flags & LRU_GEN_MASK), folio);
do {
new_gen = ((old_flags & LRU_GEN_MASK) >> LRU_GEN_PGOFF) - 1;
/* folio_update_gen() has promoted this page? */
- if (new_gen >= 0 && new_gen != old_gen)
+ if (new_gen >= 0 && new_gen != old_gen) {
+ if (increased)
+ *increased = false;
return new_gen;
+ }
new_gen = (old_gen + 1) % MAX_NR_GENS;
@@ -3317,8 +3318,21 @@ static int folio_inc_gen(struct lruvec *lruvec, struct folio *folio)
new_flags |= (new_gen + 1UL) << LRU_GEN_PGOFF;
} while (!try_cmpxchg(&folio->flags.f, &old_flags, new_flags));
- lru_gen_update_size(lruvec, folio, old_gen, new_gen);
+ if (increased)
+ *increased = true;
+ return new_gen;
+}
+static int folio_inc_gen(struct lruvec *lruvec, struct folio *folio)
+{
+ int type = folio_is_file_lru(folio);
+ struct lru_gen_folio *lrugen = &lruvec->lrugen;
+ int new_gen, old_gen = lru_gen_from_seq(lrugen->min_seq[type]);
+ bool gen_increased;
+
+ new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
+ if (gen_increased)
+ lru_gen_update_size(lruvec, folio, old_gen, new_gen);
return new_gen;
}
@@ -3904,6 +3918,7 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
struct lru_gen_folio *lrugen = &lruvec->lrugen;
int hist = lru_hist_from_seq(lrugen->min_seq[type]);
int new_gen, old_gen = lru_gen_from_seq(lrugen->min_seq[type]);
+ int target_gen = (old_gen + 1) % MAX_NR_GENS;
/* For file type, skip the check if swappiness is anon only */
if (type && (swappiness == SWAPPINESS_ANON_ONLY))
@@ -3916,32 +3931,41 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
/* prevent cold/hot inversion if the type is evictable */
for (zone = 0; zone < MAX_NR_ZONES; zone++) {
struct list_head *head = &lrugen->folios[old_gen][type][zone];
+ unsigned long delta = 0;
while (!list_empty(head)) {
struct folio *folio = lru_to_folio(head);
+ long nr_pages = folio_nr_pages(folio);
int refs = folio_lru_refs(folio);
bool workingset = folio_test_workingset(folio);
+ bool gen_increased;
VM_WARN_ON_ONCE_FOLIO(folio_test_unevictable(folio), folio);
VM_WARN_ON_ONCE_FOLIO(folio_test_active(folio), folio);
VM_WARN_ON_ONCE_FOLIO(folio_is_file_lru(folio) != type, folio);
VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
- new_gen = folio_inc_gen(lruvec, folio);
+ new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
-
+ if (gen_increased)
+ delta += nr_pages;
/* don't count the workingset being lazily promoted */
if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
int tier = lru_tier_from_refs(refs, workingset);
- int delta = folio_nr_pages(folio);
WRITE_ONCE(lrugen->protected[hist][type][tier],
- lrugen->protected[hist][type][tier] + delta);
+ lrugen->protected[hist][type][tier] + nr_pages);
}
if (!--remaining)
- return false;
+ break;
}
+ WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
+ lrugen->nr_pages[old_gen][type][zone] - delta);
+ WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
+ lrugen->nr_pages[target_gen][type][zone] + delta);
+ if (!remaining)
+ return false;
}
done:
reset_ctrl_pos(lruvec, type, true);
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 2/6] mm/mglru: batch update lrugen->protected in inc_min_seq()
2026-08-21 10:25 [PATCH 0/6] mm/mglru: speed up inc_min_seq() and fix cold/hot inversions Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 1/6] mm/mglru: batch update lrugen->nr_pages in inc_min_seq() Barry Song (Xiaomi)
@ 2026-08-21 10:25 ` Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 3/6] mm/mglru: enhance cold/hot inversion handling " Barry Song (Xiaomi)
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Barry Song (Xiaomi) @ 2026-08-21 10:25 UTC (permalink / raw)
To: akpm, linux-mm
Cc: axelrasmussen, baolin.wang, baoquan.he, chenridong, david, hannes,
kasong, lianux.mm, linux-kernel, ljs, lyugaofei, mhocko, qi.zheng,
shakeel.butt, stevensd, wangzicheng, weixugc, yuanchu, zhangbo56,
Barry Song (Xiaomi)
Avoid updating lrugen->protected with WRITE_ONCE() for each folio,
which may prevent potential compiler optimizations. Accumulate the
updates locally and apply them in a batch instead.
Signed-off-by: Barry Song (Xiaomi) <baohua@kernel.org>
---
mm/vmscan.c | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 0d74fc00abd3..99ee3c833d54 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3931,7 +3931,7 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
/* prevent cold/hot inversion if the type is evictable */
for (zone = 0; zone < MAX_NR_ZONES; zone++) {
struct list_head *head = &lrugen->folios[old_gen][type][zone];
- unsigned long delta = 0;
+ unsigned long protected[MAX_NR_TIERS] = {}, delta = 0;
while (!list_empty(head)) {
struct folio *folio = lru_to_folio(head);
@@ -3953,8 +3953,7 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
int tier = lru_tier_from_refs(refs, workingset);
- WRITE_ONCE(lrugen->protected[hist][type][tier],
- lrugen->protected[hist][type][tier] + nr_pages);
+ protected[tier] += nr_pages;
}
if (!--remaining)
@@ -3964,6 +3963,9 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
lrugen->nr_pages[old_gen][type][zone] - delta);
WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
lrugen->nr_pages[target_gen][type][zone] + delta);
+ for (int tier = 0; tier < MAX_NR_TIERS; tier++)
+ WRITE_ONCE(lrugen->protected[hist][type][tier],
+ lrugen->protected[hist][type][tier] + protected[tier]);
if (!remaining)
return false;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 3/6] mm/mglru: enhance cold/hot inversion handling in inc_min_seq()
2026-08-21 10:25 [PATCH 0/6] mm/mglru: speed up inc_min_seq() and fix cold/hot inversions Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 1/6] mm/mglru: batch update lrugen->nr_pages in inc_min_seq() Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 2/6] mm/mglru: batch update lrugen->protected " Barry Song (Xiaomi)
@ 2026-08-21 10:25 ` Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 4/6] mm/mglru: exclude folios promoted by aging from protected " Barry Song (Xiaomi)
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Barry Song (Xiaomi) @ 2026-08-21 10:25 UTC (permalink / raw)
To: akpm, linux-mm
Cc: axelrasmussen, baolin.wang, baoquan.he, chenridong, david, hannes,
kasong, lianux.mm, linux-kernel, ljs, lyugaofei, mhocko, qi.zheng,
shakeel.butt, stevensd, wangzicheng, weixugc, yuanchu, zhangbo56,
Barry Song (Xiaomi)
During aging, a folio's generation may already have been updated by
folio_update_gen(), even though it has not yet been moved to the
corresponding generation list. Such folios are hotter than those
already in that generation.
It makes sense for inc_min_seq() to increment the generation of
folios that were never promoted during aging and move them to the
tail of the new oldest generation. However, folios that were already
promoted should instead be moved to the head of their updated
generation, just as sort_folio() does in scan_folios().
Otherwise, promoted folios could end up behind folios that were
never promoted, effectively inverting their hot/cold ordering.
Signed-off-by: Barry Song (Xiaomi) <baohua@kernel.org>
---
mm/vmscan.c | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 99ee3c833d54..3b618a51cde2 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3946,9 +3946,12 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
- list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
- if (gen_increased)
+ if (gen_increased) {
delta += nr_pages;
+ list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+ } else {
+ list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+ }
/* don't count the workingset being lazily promoted */
if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
int tier = lru_tier_from_refs(refs, workingset);
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 4/6] mm/mglru: exclude folios promoted by aging from protected in inc_min_seq()
2026-08-21 10:25 [PATCH 0/6] mm/mglru: speed up inc_min_seq() and fix cold/hot inversions Barry Song (Xiaomi)
` (2 preceding siblings ...)
2026-08-21 10:25 ` [PATCH 3/6] mm/mglru: enhance cold/hot inversion handling " Barry Song (Xiaomi)
@ 2026-08-21 10:25 ` Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 5/6] mm/mglru: move folios from oldest gen to second-oldest gen from head to tail Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 6/6] mm/mglru: batch move folios to the second-oldest gen's LRU Barry Song (Xiaomi)
5 siblings, 0 replies; 7+ messages in thread
From: Barry Song (Xiaomi) @ 2026-08-21 10:25 UTC (permalink / raw)
To: akpm, linux-mm
Cc: axelrasmussen, baolin.wang, baoquan.he, chenridong, david, hannes,
kasong, lianux.mm, linux-kernel, ljs, lyugaofei, mhocko, qi.zheng,
shakeel.butt, stevensd, wangzicheng, weixugc, yuanchu, zhangbo56,
Barry Song (Xiaomi)
Some folios may have been promoted during aging, so don't count them
as protected, similar to sort_folio().
Signed-off-by: Barry Song (Xiaomi) <baohua@kernel.org>
---
mm/vmscan.c | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 3b618a51cde2..7bd01875fade 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3949,16 +3949,15 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
if (gen_increased) {
delta += nr_pages;
list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+ /* don't count the workingset being lazily promoted */
+ if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
+ int tier = lru_tier_from_refs(refs, workingset);
+
+ protected[tier] += nr_pages;
+ }
} else {
list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
}
- /* don't count the workingset being lazily promoted */
- if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
- int tier = lru_tier_from_refs(refs, workingset);
-
- protected[tier] += nr_pages;
- }
-
if (!--remaining)
break;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 5/6] mm/mglru: move folios from oldest gen to second-oldest gen from head to tail
2026-08-21 10:25 [PATCH 0/6] mm/mglru: speed up inc_min_seq() and fix cold/hot inversions Barry Song (Xiaomi)
` (3 preceding siblings ...)
2026-08-21 10:25 ` [PATCH 4/6] mm/mglru: exclude folios promoted by aging from protected " Barry Song (Xiaomi)
@ 2026-08-21 10:25 ` Barry Song (Xiaomi)
2026-08-21 10:25 ` [PATCH 6/6] mm/mglru: batch move folios to the second-oldest gen's LRU Barry Song (Xiaomi)
5 siblings, 0 replies; 7+ messages in thread
From: Barry Song (Xiaomi) @ 2026-08-21 10:25 UTC (permalink / raw)
To: akpm, linux-mm
Cc: axelrasmussen, baolin.wang, baoquan.he, chenridong, david, hannes,
kasong, lianux.mm, linux-kernel, ljs, lyugaofei, mhocko, qi.zheng,
shakeel.butt, stevensd, wangzicheng, weixugc, yuanchu, zhangbo56,
Barry Song (Xiaomi)
For reclamation, it makes sense to reclaim folios from tail to
head, as folios near the head are relatively hot. However, when
moving folios from the oldest generation to the second-oldest
generation, using the tail-to-head order would effectively cause
a cold/hot inversion.
Signed-off-by: Barry Song (Xiaomi) <baohua@kernel.org>
---
mm/vmscan.c | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 7bd01875fade..2fd82b2ca4d1 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -191,8 +191,20 @@ struct scan_control {
prefetchw(&prev->_field); \
} \
} while (0)
+#define prefetchw_next_lru_folio(_folio, _base, _field) \
+ do { \
+ if ((_folio)->lru.next != _base) { \
+ struct folio *next; \
+ \
+ next = list_entry((_folio)->lru.next, \
+ struct folio, lru); \
+ prefetchw(&next->_field); \
+ } \
+ } while (0)
+
#else
#define prefetchw_prev_lru_folio(_folio, _base, _field) do { } while (0)
+#define prefetchw_next_lru_folio(_folio, _base, _field) do { } while (0)
#endif
/*
@@ -3932,9 +3944,10 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
for (zone = 0; zone < MAX_NR_ZONES; zone++) {
struct list_head *head = &lrugen->folios[old_gen][type][zone];
unsigned long protected[MAX_NR_TIERS] = {}, delta = 0;
+ struct list_head *pos = head->next;
- while (!list_empty(head)) {
- struct folio *folio = lru_to_folio(head);
+ while (pos != head) {
+ struct folio *folio = list_entry(pos, struct folio, lru);
long nr_pages = folio_nr_pages(folio);
int refs = folio_lru_refs(folio);
bool workingset = folio_test_workingset(folio);
@@ -3945,6 +3958,8 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
VM_WARN_ON_ONCE_FOLIO(folio_is_file_lru(folio) != type, folio);
VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
+ prefetchw_next_lru_folio(folio, head, flags);
+ pos = pos->next;
new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
if (gen_increased) {
delta += nr_pages;
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 6/6] mm/mglru: batch move folios to the second-oldest gen's LRU
2026-08-21 10:25 [PATCH 0/6] mm/mglru: speed up inc_min_seq() and fix cold/hot inversions Barry Song (Xiaomi)
` (4 preceding siblings ...)
2026-08-21 10:25 ` [PATCH 5/6] mm/mglru: move folios from oldest gen to second-oldest gen from head to tail Barry Song (Xiaomi)
@ 2026-08-21 10:25 ` Barry Song (Xiaomi)
5 siblings, 0 replies; 7+ messages in thread
From: Barry Song (Xiaomi) @ 2026-08-21 10:25 UTC (permalink / raw)
To: akpm, linux-mm
Cc: axelrasmussen, baolin.wang, baoquan.he, chenridong, david, hannes,
kasong, lianux.mm, linux-kernel, ljs, lyugaofei, mhocko, qi.zheng,
shakeel.butt, stevensd, wangzicheng, weixugc, yuanchu, zhangbo56,
Barry Song (Xiaomi)
Detect folios that need to move from the oldest generation to
the second-oldest generation, and batch-move them together.
This can significantly reduce the sys time of inc_min_seq(),
especially when the other type is significantly behind the
preferred type.
Assisted-by: gemini:gemini-3.6-flash
Signed-off-by: Barry Song (Xiaomi) <baohua@kernel.org>
---
mm/vmscan.c | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 2fd82b2ca4d1..996b48344ed0 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3923,6 +3923,19 @@ static void clear_mm_walk(void)
kfree(walk);
}
+static inline void flush_lru_batch(struct list_head *head, struct list_head **batch_end,
+ struct list_head *dst)
+{
+ LIST_HEAD(movable);
+
+ if (!*batch_end)
+ return;
+
+ list_cut_position(&movable, head, *batch_end);
+ list_splice_tail_init(&movable, dst);
+ *batch_end = NULL;
+}
+
static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
{
int zone;
@@ -3942,9 +3955,11 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
/* prevent cold/hot inversion if the type is evictable */
for (zone = 0; zone < MAX_NR_ZONES; zone++) {
+ struct list_head *target_list = &lrugen->folios[target_gen][type][zone];
struct list_head *head = &lrugen->folios[old_gen][type][zone];
unsigned long protected[MAX_NR_TIERS] = {}, delta = 0;
struct list_head *pos = head->next;
+ struct list_head *batch_end = NULL;
while (pos != head) {
struct folio *folio = list_entry(pos, struct folio, lru);
@@ -3963,7 +3978,8 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
if (gen_increased) {
delta += nr_pages;
- list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+ batch_end = &folio->lru;
+
/* don't count the workingset being lazily promoted */
if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
int tier = lru_tier_from_refs(refs, workingset);
@@ -3971,11 +3987,14 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
protected[tier] += nr_pages;
}
} else {
+ flush_lru_batch(head, &batch_end, target_list);
list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
}
if (!--remaining)
break;
}
+ flush_lru_batch(head, &batch_end, target_list);
+
WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
lrugen->nr_pages[old_gen][type][zone] - delta);
WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread