Skip to contentSkip to conversation

Refault distance update with MGLRU support

RFC PATCH New Kairui Song 2 participants 11 messages Sep 20, 2023 – Sep 21, 2023

Subsystems (5)
lore
Share & tools
lore
#0 lore raw

v3 cover Refault distance update with MGLRU support

From: Kairui Song <[email protected]>

I noticed MGLRU not working very well on certain workflows, which is observed on some workloads with heavy memory stress.

After some debugging, I found this was related to refault distance detection, when the file page workingset size exceeds total memory, and the access distance (the left-shift time of a page before it gets activated or promoted, considering LRU starts from right) of file pages are larger than total memory. All file pages are stuck on the oldest generation and getting read-in then evicted permutably, few get activated and stay in memory.

This series tries to fix this problem by rework the refault distance based activation to better fit MGLRU, and also tries to use a unified algorithm for both MGLRU and Inactive/Active LRU, the performance almost doubled for the workloads that are not working well previously.

Patch 1/5 reworked the refault distance detection model for Inactive/Active LRU, and updated the comments.

Patch 2/5 splitted the code logic into a helper, prepare for MGLRU.

Patch 3/5 and 4/5 are code simplification and updates for MGLRU.

Patch 5/5 applies the modified refault distance algorithm for MGLRU.

Following benchmark showed 5x improvement:
To simulate the workflow, I setup a 3-replicated mongodb cluster using docker, each in a standalone cgroup, set to use 5GB of wiretiger cache and 10g of oplog, on a 32G VM. The benchmark is done using
https://github.com/apavlo/py-tpcc.git, modified to run STOCK_LEVEL query only, for simulating slow query and get a stable result.

Before (with ZRAM enabled, the result won't change whether any kind of swap is on or not):

$ tpcc.py --config=mongodb.config mongodb --duration=900 --warehouses=500 --clients=30
==================================================================
Execution Results after 919 seconds
------------------------------------------------------------------
                  Executed        Time (µs)       Rate
  STOCK_LEVEL     577             27584645283.7   0.02 txn/s
------------------------------------------------------------------
  TOTAL           577             27584645283.7   0.02 txn/s

Patched (with ZRAM enabled):

$ tpcc.py --config=mongodb.config mongodb --duration=900 --warehouses=500 --clients=30
==================================================================
Execution Results after 905 seconds
------------------------------------------------------------------
                  Executed        Time (µs)       Rate
  STOCK_LEVEL     2542            27121571486.2   0.09 txn/s
------------------------------------------------------------------
  TOTAL           2542            27121571486.2   0.09 txn/s

The performance is 5x times better than before. Testing with lower stress and some other benchmarks also shows slight improvement or equivalent performance (eg. fio tests shows a observable performance gain).

Sending out as RFC, I'm still doing more test on it, since this changed a frequently used algorithm and not really sure if there is any performance regression on long term. It should improvement the performance for file pages in general even if there are low memory pressure, since it saved some cgroup iterations and atomic operations.

Update from V2:
- Rebase to latest mm-stable and redone some tests.
- Split the algorithm change into a stand alone patch as suggested by Johannes Weiner.

Update from V1:
- Removed the fls operations which previously used in patch 1 for protecting active pages by expontial ratio, simply compare with number of inactive pages seems good enough.
- Update some benchmarks results, test result that are basically identical as before are not updated.

Kairui Song (6):
workingset: simplify and use a more intuitive model
workingset: move refault distance checking into to a helper
workignset: simplify the initilization code
workingset: simplify lru_gen_test_recent
mm, lru_gen: convert avg_total and avg_refaulted to atomic workingset, lru_gen: apply refault-distance based re-activation

 include/linux/mmzone.h |   4 +-
 include/linux/swap.h   |   2 -
 mm/swap.c              |   1 -
 mm/vmscan.c            |  30 +--
 mm/workingset.c        | 416 +++++++++++++++++++++--------------------
 5 files changed, 236 insertions(+), 217 deletions(-)
Signature · 1 line
2.41.0
#1 lore raw

RFC PATCH v3 1/6 workingset: simplify and use a more intuitive model

From: Kairui Song <[email protected]>

This basically removed workingset_activation and reduced calls to workingset_age_nonresident.

The idea behind this change is a new way to calculate the refault distance and prepare for adapting refault distance based re-activation for multi-gen LRU.

Currently, refault distance re-activation is based on two assumptions:
1. Activation of an inactive page will left-shift LRU pages (considering LRU starts from right).
2. Eviction of an inactive page will left-shift LRU pages.

Assumption 2 is correct, but assumption 1 is not always true, an activated page could be anywhere in the LRU list (through mark_page_accessed), it only left-shift the pages on its right.

And besides, one page can get activate/deactivated for multiple times.

And multi-gen LRU doesn't fit with this model well, pages are getting aged and activated constantly as the generation sliding window slides.

So instead we introduce a simpler idea here: Just presume the evicted pages are still in memory, each has an eviction sequence like before. Let the nonresistence_age still be NA and get increased for each eviction, so we get a "Shadow LRU" here of one evicted page:

Let SP = ((NA's reading @ current) - (NA's reading @ eviction))

                           +-memory available to cache-+
                           |                           |
 +-------------------------+===============+===========+
 | *   shadows  O O  O     |   INACTIVE    |   ACTIVE  |
 +-+-----------------------+===============+===========+
   |                       |
   +-----------------------+
   |         SP
 fault page          O -> Hole left by previously faulted in pages

* -> The page corresponding to SP

It can be easily seen that SP stands for how far the current workflow could push a page out of available memory. Since all evicted page was once head of INACTIVE list, the page could have such an access distance:

SP + NR_INACTIVE

It *may* get re-activated before getting evicted again if:

SP + NR_INACTIVE < NR_INACTIVE + NR_ACTIVE

Which can be simplified to:

SP < NR_ACTIVE

Then the page is worth getting re-activated to start from ACTIVE part, since the access distance is shorter than the total memory to make it stay.

And since this is only an estimation, based on several hypotheses, and it could break the ability of LRU to distinguish a workingset out of caches, so throttle this by two factors:

1. Notice previously re-faulted in pages may leave "holes" on the shadow part of LRU, that part is left unhandled on purpose to decrease re-activate rate for pages that have a large SP value (the larger SP value a page has, the more likely it will be affected by such holes).
2. When the ACTIVE part of LRU is long enough, chanllaging ACTIVE pages by re-activating a one-time faulted previously INACTIVE page may not be a good idea, so throttle the re-activation when ACTIVE > INACTIVE by comparing with INACTIVE instead.

Another effect of the refault activation worth noticing is that, by throttling reactivation when ACTIVE part is high, this refault distance based re-activation can help hold a portion of the caches in memory instead of letting cached get evicted permutably when the cache size is larger than total memory, and hotness is similar among all cache pages. That's because the established workingset (ACTIVE part) will tend to stay since we throttled reactivation, until the workingset itself start to stall.

This is actually similar with the algoritm before, which introduce such effect by increasing nonresistence_age in many call paths, trottled the reactivation when activition/reactivation is massively happenning.

Combined all above, we have:
Upon refault, if any of following conditions is met, mark page as active:

- If ACTIVE LRU is low (NR_ACTIVE < NR_INACTIVE), check if:
SP < NR_ACTIVE

- If ACTIVE LRU is high (NR_ACTIVE >= NR_INACTIVE), check if:
SP < NR_INACTIVE

Code-wise, this is simpler than before since no longer need to do lruvec statistic update when activating a page, and so far, a few benchmarks shows a similar or better result. And when combined with multi-gen LRU (in later commits) it shows a measurable performance gain for some workloads.

Using memtier and fio test from commit ac35a4902374 but scaled down to fit in my test environment, and some other test results:

  memtier test (with 16G ramdisk as swap and 4G memcg limit on an i7-9700):
  memcached -u nobody -m 16384 -s /tmp/memcached.socket \
    -a 0766 -t 12 -B binary &
  memtier_benchmark -S /tmp/memcached.socket -P memcache_binary -n allkeys\
    --key-minimum=1 --key-maximum=32000000 --key-pattern=P:P -c 1 \
    -t 12 --ratio 1:0 --pipeline 8 -d 2000 -x 6

  fio test 1 (with 16G ramdisk on 28G VM on an i7-9700):
  fio -name=refault --numjobs=12 --directory=/mnt --size=1024m \
    --buffered=1 --ioengine=io_uring --iodepth=128 \
    --iodepth_batch_submit=32 --iodepth_batch_complete=32 \
    --rw=randread --random_distribution=random --norandommap \
    --time_based --ramp_time=5m --runtime=5m --group_reporting

  fio test 2 (with 16G ramdisk on 28G VM on an i7-9700):
  fio -name=mglru --numjobs=10 --directory=/mnt --size=1536m \
    --buffered=1 --ioengine=io_uring --iodepth=128 \
    --iodepth_batch_submit=32 --iodepth_batch_complete=32 \
    --rw=randread --random_distribution=zipf:1.2 --norandommap \
    --time_based --ramp_time=10m --runtime=5m --group_reporting

mysql (using oltp_read_only from sysbench, with 12G of buffer pool in a 10G memcg):

  sysbench /usr/share/sysbench/oltp_read_only.lua <auth and db params> \
    --tables=36 --table-size=2000000 --threads=12 --time=1800

kernel build test done with 3G memcg limit on an i7-9700.

Before (Average of 6 test run):
fio: IOPS=5125.5k
fio2: IOPS=7291.16k
memcached: 57600.926 ops/s
mysql: 6491.5 tps
kernel-build: 1817.13499 seconds

After (Average of 6 test run):
fio: IOPS=5137.5k
fio2: IOPS=7300.67k
memcached: 57878.422 ops/s
mysql: 6491.1 tps
kernel-build: 1813.66231 seconds

---
 include/linux/swap.h |   2 -
 mm/swap.c            |   1 -
 mm/vmscan.c          |   2 -
 mm/workingset.c      | 155 ++++++++++++++++++-------------------------
 4 files changed, 64 insertions(+), 96 deletions(-)
include/linux/swap.h +0 -2
index 493487ed7c38..ca51d79842b7 100644
@@ -344,10 +344,8 @@ static inline swp_entry_t page_swap_entry(struct page *page)
 
 /* linux/mm/workingset.c */
 bool workingset_test_recent(void *shadow, bool file, bool *workingset);
-void workingset_age_nonresident(struct lruvec *lruvec, unsigned long nr_pages);
 void *workingset_eviction(struct folio *folio, struct mem_cgroup *target_memcg);
 void workingset_refault(struct folio *folio, void *shadow);
-void workingset_activation(struct folio *folio);
 
 /* Only track the nodes of mappings with shadow entries */
 void workingset_update_node(struct xa_node *node);
mm/swap.c +0 -1
index cd8f0150ba3a..685b446fd4f9 100644
@@ -482,7 +482,6 @@ void folio_mark_accessed(struct folio *folio)
 		else
 			__lru_cache_activate_folio(folio);
 		folio_clear_referenced(folio);
-		workingset_activation(folio);
 	}
 	if (folio_test_idle(folio))
 		folio_clear_idle(folio);
mm/vmscan.c +0 -2
index 6f13394b112e..3f4de75e5186 100644
@@ -2539,8 +2539,6 @@ static unsigned int move_folios_to_lru(struct lruvec *lruvec,
 		lruvec_add_folio(lruvec, folio);
 		nr_pages = folio_nr_pages(folio);
 		nr_moved += nr_pages;
-		if (folio_test_active(folio))
-			workingset_age_nonresident(lruvec, nr_pages);
 	}
 
 	/*
mm/workingset.c +64 -91

204 lines not on this page · raw message

Signature · 1 line
2.41.0
#2 lore raw

Re: [RFC PATCH v3 1/6] workingset: simplify and use a more intuitive model

kernel test robot noticed the following build warnings:

Hi Kairui,

[This is a private test report for your RFC patch.] kernel test robot noticed the following build warnings:

[auto build test WARNING on linus/master]
[also build test WARNING on v6.6-rc2 next-20230921]
[cannot apply to akpm-mm/mm-everything]
[If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Kairui-Song/workingset-simplify-and-use-a-more-intuitive-model/20230921-030515
base:   linus/master
patch link:    https://lore.kernel.org/r/20230920190244.16839-2-ryncsn%40gmail.com
patch subject: [RFC PATCH v3 1/6] workingset: simplify and use a more intuitive model
config: mips-pic32mzda_defconfig (https://download.01.org/0day-ci/archive/20230921/[email protected]/config)
compiler: clang version 17.0.0 (https://github.com/llvm/llvm-project.git 4a5ac14ee968ff0ad5d2cc1ffa0299048db4c88a)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20230921/[email protected]/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/

All warnings (new ones prefixed by >>):

>> mm/workingset.c:345:6: warning: no previous prototype for function 'workingset_age_nonresident' [-Wmissing-prototypes]
     345 | void workingset_age_nonresident(struct lruvec *lruvec, unsigned long nr_pages)
         |      ^
   mm/workingset.c:345:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
     345 | void workingset_age_nonresident(struct lruvec *lruvec, unsigned long nr_pages)
         | ^
         | static 

1 warning generated.

vim +/workingset_age_nonresident +345 mm/workingset.c

ac35a490237446 Yu Zhao         2022-09-18  334  
31d8fcac00fcf4 Johannes Weiner 2020-06-25  335  /**
31d8fcac00fcf4 Johannes Weiner 2020-06-25  336   * workingset_age_nonresident - age non-resident entries as LRU ages
e755f4af08b74e Xiaofei Tan     2020-10-15  337   * @lruvec: the lruvec that was aged
31d8fcac00fcf4 Johannes Weiner 2020-06-25  338   * @nr_pages: the number of pages to count
31d8fcac00fcf4 Johannes Weiner 2020-06-25  339   *
31d8fcac00fcf4 Johannes Weiner 2020-06-25  340   * As in-memory pages are aged, non-resident pages need to be aged as
31d8fcac00fcf4 Johannes Weiner 2020-06-25  341   * well, in order for the refault distances later on to be comparable
31d8fcac00fcf4 Johannes Weiner 2020-06-25  342   * to the in-memory dimensions. This function allows reclaim and LRU
31d8fcac00fcf4 Johannes Weiner 2020-06-25  343   * operations to drive the non-resident aging along in parallel.
31d8fcac00fcf4 Johannes Weiner 2020-06-25  344   */
31d8fcac00fcf4 Johannes Weiner 2020-06-25 @345  void workingset_age_nonresident(struct lruvec *lruvec, unsigned long nr_pages)
b910718a948a91 Johannes Weiner 2019-11-30  346  {
b910718a948a91 Johannes Weiner 2019-11-30  347  	/*
b910718a948a91 Johannes Weiner 2019-11-30  348  	 * Reclaiming a cgroup means reclaiming all its children in a
b910718a948a91 Johannes Weiner 2019-11-30  349  	 * round-robin fashion. That means that each cgroup has an LRU
b910718a948a91 Johannes Weiner 2019-11-30  350  	 * order that is composed of the LRU orders of its child
b910718a948a91 Johannes Weiner 2019-11-30  351  	 * cgroups; and every page has an LRU position not just in the
b910718a948a91 Johannes Weiner 2019-11-30  352  	 * cgroup that owns it, but in all of that group's ancestors.
b910718a948a91 Johannes Weiner 2019-11-30  353  	 *
b910718a948a91 Johannes Weiner 2019-11-30  354  	 * So when the physical inactive list of a leaf cgroup ages,
b910718a948a91 Johannes Weiner 2019-11-30  355  	 * the virtual inactive lists of all its parents, including
b910718a948a91 Johannes Weiner 2019-11-30  356  	 * the root cgroup's, age as well.
b910718a948a91 Johannes Weiner 2019-11-30  357  	 */
b910718a948a91 Johannes Weiner 2019-11-30  358  	do {
31d8fcac00fcf4 Johannes Weiner 2020-06-25  359  		atomic_long_add(nr_pages, &lruvec->nonresident_age);
31d8fcac00fcf4 Johannes Weiner 2020-06-25  360  	} while ((lruvec = parent_lruvec(lruvec)));
b910718a948a91 Johannes Weiner 2019-11-30  361  }

b910718a948a91 Johannes Weiner 2019-11-30 362

Signature · 2 lines
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
#3 lore raw

Re: [RFC PATCH v3 1/6] workingset: simplify and use a more intuitive model

In reply to Kairui Song
kernel test robot noticed the following build warnings:

Hi Kairui,

[This is a private test report for your RFC patch.] kernel test robot noticed the following build warnings:

[auto build test WARNING on linus/master]
[also build test WARNING on v6.6-rc2 next-20230921]
[cannot apply to akpm-mm/mm-everything]
[If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Kairui-Song/workingset-simplify-and-use-a-more-intuitive-model/20230921-030515
base:   linus/master
patch link:    https://lore.kernel.org/r/20230920190244.16839-2-ryncsn%40gmail.com
patch subject: [RFC PATCH v3 1/6] workingset: simplify and use a more intuitive model
config: x86_64-randconfig-074-20230921 (https://download.01.org/0day-ci/archive/20230921/[email protected]/config)
compiler: gcc-7 (Ubuntu 7.5.0-6ubuntu2) 7.5.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20230921/[email protected]/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/

All warnings (new ones prefixed by >>):

>> mm/workingset.c:345:6: warning: no previous declaration for 'workingset_age_nonresident' [-Wmissing-declarations]

void workingset_age_nonresident(struct lruvec *lruvec, unsigned long nr_pages)
^~~~~~~~~~~~~~~~~~~~~~~~~~

vim +/workingset_age_nonresident +345 mm/workingset.c

ac35a490237446 Yu Zhao         2022-09-18  334  
31d8fcac00fcf4 Johannes Weiner 2020-06-25  335  /**
31d8fcac00fcf4 Johannes Weiner 2020-06-25  336   * workingset_age_nonresident - age non-resident entries as LRU ages
e755f4af08b74e Xiaofei Tan     2020-10-15  337   * @lruvec: the lruvec that was aged
31d8fcac00fcf4 Johannes Weiner 2020-06-25  338   * @nr_pages: the number of pages to count
31d8fcac00fcf4 Johannes Weiner 2020-06-25  339   *
31d8fcac00fcf4 Johannes Weiner 2020-06-25  340   * As in-memory pages are aged, non-resident pages need to be aged as
31d8fcac00fcf4 Johannes Weiner 2020-06-25  341   * well, in order for the refault distances later on to be comparable
31d8fcac00fcf4 Johannes Weiner 2020-06-25  342   * to the in-memory dimensions. This function allows reclaim and LRU
31d8fcac00fcf4 Johannes Weiner 2020-06-25  343   * operations to drive the non-resident aging along in parallel.
31d8fcac00fcf4 Johannes Weiner 2020-06-25  344   */
31d8fcac00fcf4 Johannes Weiner 2020-06-25 @345  void workingset_age_nonresident(struct lruvec *lruvec, unsigned long nr_pages)
b910718a948a91 Johannes Weiner 2019-11-30  346  {
b910718a948a91 Johannes Weiner 2019-11-30  347  	/*
b910718a948a91 Johannes Weiner 2019-11-30  348  	 * Reclaiming a cgroup means reclaiming all its children in a
b910718a948a91 Johannes Weiner 2019-11-30  349  	 * round-robin fashion. That means that each cgroup has an LRU
b910718a948a91 Johannes Weiner 2019-11-30  350  	 * order that is composed of the LRU orders of its child
b910718a948a91 Johannes Weiner 2019-11-30  351  	 * cgroups; and every page has an LRU position not just in the
b910718a948a91 Johannes Weiner 2019-11-30  352  	 * cgroup that owns it, but in all of that group's ancestors.
b910718a948a91 Johannes Weiner 2019-11-30  353  	 *
b910718a948a91 Johannes Weiner 2019-11-30  354  	 * So when the physical inactive list of a leaf cgroup ages,
b910718a948a91 Johannes Weiner 2019-11-30  355  	 * the virtual inactive lists of all its parents, including
b910718a948a91 Johannes Weiner 2019-11-30  356  	 * the root cgroup's, age as well.
b910718a948a91 Johannes Weiner 2019-11-30  357  	 */
b910718a948a91 Johannes Weiner 2019-11-30  358  	do {
31d8fcac00fcf4 Johannes Weiner 2020-06-25  359  		atomic_long_add(nr_pages, &lruvec->nonresident_age);
31d8fcac00fcf4 Johannes Weiner 2020-06-25  360  	} while ((lruvec = parent_lruvec(lruvec)));
b910718a948a91 Johannes Weiner 2019-11-30  361  }

b910718a948a91 Johannes Weiner 2019-11-30 362

Signature · 2 lines
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
#4 lore raw

Re: [RFC PATCH v3 1/6] workingset: simplify and use a more intuitive model

In reply to Kairui Song
kernel test robot noticed the following build warnings:

Hi Kairui,

[This is a private test report for your RFC patch.] kernel test robot noticed the following build warnings:

[auto build test WARNING on linus/master]
[also build test WARNING on v6.6-rc2 next-20230921]
[cannot apply to akpm-mm/mm-everything]
[If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Kairui-Song/workingset-simplify-and-use-a-more-intuitive-model/20230921-030515
base:   linus/master
patch link:    https://lore.kernel.org/r/20230920190244.16839-2-ryncsn%40gmail.com
patch subject: [RFC PATCH v3 1/6] workingset: simplify and use a more intuitive model
config: x86_64-randconfig-123-20230921 (https://download.01.org/0day-ci/archive/20230922/[email protected]/config)
compiler: gcc-12 (Debian 12.2.0-14) 12.2.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20230922/[email protected]/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/

sparse warnings: (new ones prefixed by >>)

>> mm/workingset.c:345:6: sparse: sparse: symbol 'workingset_age_nonresident' was not declared. Should it be static?

mm/workingset.c:724:9: sparse: sparse: context imbalance in 'shadow_lru_isolate' - different lock contexts for basic block

Signature · 2 lines
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
#5 lore raw

RFC PATCH v3 2/6 workingset: move refault distance checking into to a helper

In reply to Kairui Song

From: Kairui Song <[email protected]>

There isn't any feature change, just move the refault distance checking logic into a standalone helper so it can be reused later.

---
 mm/workingset.c | 137 ++++++++++++++++++++++++++++--------------------
 1 file changed, 79 insertions(+), 58 deletions(-)
mm/workingset.c +79 -58
index 8613945fc66e..b0704cbfc667 100644
@@ -170,9 +170,10 @@
  */
 
 #define WORKINGSET_SHIFT 1
-#define EVICTION_SHIFT	((BITS_PER_LONG - BITS_PER_XA_VALUE) +	\
+#define EVICTION_SHIFT	((BITS_PER_LONG - BITS_PER_XA_VALUE) + \
 			 WORKINGSET_SHIFT + NODES_SHIFT + \
 			 MEM_CGROUP_ID_SHIFT)
+#define EVICTION_BITS	(BITS_PER_LONG - (EVICTION_SHIFT))
 #define EVICTION_MASK	(~0UL >> EVICTION_SHIFT)
 
 /*
@@ -216,6 +217,79 @@ static void unpack_shadow(void *shadow, int *memcgidp, pg_data_t **pgdat,
 	*workingsetp = workingset;
 }
 
+/*
+ * Get the refault distance timestamp reading at eviction time.
+ */
+static inline unsigned long lru_eviction(struct lruvec *lruvec,
+					 int bits, int bucket_order)
+{
+	unsigned long eviction = atomic_long_read(&lruvec->nonresident_age);
+
+	eviction >>= bucket_order;
+	eviction &= ~0UL >> (BITS_PER_LONG - bits);
+
+	return eviction;
+}
+
+/*
+ * Calculate and test refault distance.
+ */
+static inline bool lru_test_refault(struct mem_cgroup *memcg,
+				    struct lruvec *lruvec,
+				    unsigned long eviction, bool file,
+				    int bits, int bucket_order)
+{
+	unsigned long refault, distance;
+	unsigned long active, inactive_file, inactive_anon;
+
+	eviction <<= bucket_order;
+	refault = atomic_long_read(&lruvec->nonresident_age);
+
+	/*
+	 * The unsigned subtraction here gives an accurate distance
+	 * across nonresident_age overflows in most cases. There is a
+	 * special case: usually, shadow entries have a short lifetime
+	 * and are either refaulted or reclaimed along with the inode
+	 * before they get too old.  But it is not impossible for the
+	 * nonresident_age to lap a shadow entry in the field, which
+	 * can then result in a false small refault distance, leading
+	 * to a false activation should this old entry actually
+	 * refault again.  However, earlier kernels used to deactivate
+	 * unconditionally with *every* reclaim invocation for the
+	 * longest time, so the occasional inappropriate activation
+	 * leading to pressure on the active list is not a problem.
+	 */
+	distance = (refault - eviction) & (~0UL >> (BITS_PER_LONG - bits));
+
+	/*
+	 * Compare the distance to the existing workingset size. We
+	 * don't activate pages that couldn't stay resident even if
+	 * all the memory was available to the workingset. Whether
+	 * workingset competition needs to consider anon or not depends
+	 * on having free swap space.
+	 */
+	active = lruvec_page_state(lruvec, NR_ACTIVE_FILE);
+	inactive_file = lruvec_page_state(lruvec, NR_INACTIVE_FILE);
+
+	if (mem_cgroup_get_nr_swap_pages(memcg) > 0) {
+		active += lruvec_page_state(lruvec, NR_ACTIVE_ANON);
+		inactive_anon = lruvec_page_state(lruvec, NR_INACTIVE_ANON);
+	} else {
+		inactive_anon = 0;
+	}
+
+	/*
+	 * When there are already enough active pages, be less aggressive
+	 * on reactivating pages, challenge an large set of established
+	 * active pages with one time refaulted page may not be a good idea.
+	 */
+	if (active >= inactive_anon + inactive_file)
+		return distance < inactive_anon + inactive_file;
+	else
+		return distance < active + \
+			(file ? inactive_anon : inactive_file);
+}
+
 #ifdef CONFIG_LRU_GEN
 
 static void *lru_gen_eviction(struct folio *folio)
@@ -386,11 +460,10 @@ void *workingset_eviction(struct folio *folio, struct mem_cgroup *target_memcg)
 	lruvec = mem_cgroup_lruvec(target_memcg, pgdat);
 	/* XXX: target_memcg can be NULL, go through lruvec */
 	memcgid = mem_cgroup_id(lruvec_memcg(lruvec));
-	eviction = atomic_long_read(&lruvec->nonresident_age);
-	eviction >>= bucket_order;
+	eviction = lru_eviction(lruvec, EVICTION_BITS, bucket_order);
 	workingset_age_nonresident(lruvec, folio_nr_pages(folio));
 	return pack_shadow(memcgid, pgdat, eviction,
-				folio_test_workingset(folio));
+			   folio_test_workingset(folio));
 }
 
 /**
@@ -408,11 +481,6 @@ bool workingset_test_recent(void *shadow, bool file, bool *workingset)
 {
 	struct mem_cgroup *eviction_memcg;
 	struct lruvec *eviction_lruvec;
-	unsigned long refault_distance;
-	unsigned long inactive_file;
-	unsigned long inactive_anon;
-	unsigned long refault;
-	unsigned long active;
 	int memcgid;
 	struct pglist_data *pgdat;
 	unsigned long eviction;
@@ -421,7 +489,6 @@ bool workingset_test_recent(void *shadow, bool file, bool *workingset)
 		return lru_gen_test_recent(shadow, file, &eviction_lruvec, &eviction, workingset);
 
 	unpack_shadow(shadow, &memcgid, &pgdat, &eviction, workingset);
-	eviction <<= bucket_order;
 
 	/*
 	 * Look up the memcg associated with the stored ID. It might
@@ -442,56 +509,10 @@ bool workingset_test_recent(void *shadow, bool file, bool *workingset)
 	eviction_memcg = mem_cgroup_from_id(memcgid);
 	if (!mem_cgroup_disabled() && !eviction_memcg)
 		return false;
-
 	eviction_lruvec = mem_cgroup_lruvec(eviction_memcg, pgdat);
-	refault = atomic_long_read(&eviction_lruvec->nonresident_age);
 
-	/*
-	 * Calculate the refault distance
-	 *
-	 * The unsigned subtraction here gives an accurate distance
-	 * across nonresident_age overflows in most cases. There is a
-	 * special case: usually, shadow entries have a short lifetime
-	 * and are either refaulted or reclaimed along with the inode
-	 * before they get too old.  But it is not impossible for the
-	 * nonresident_age to lap a shadow entry in the field, which
-	 * can then result in a false small refault distance, leading
-	 * to a false activation should this old entry actually
-	 * refault again.  However, earlier kernels used to deactivate
-	 * unconditionally with *every* reclaim invocation for the
-	 * longest time, so the occasional inappropriate activation
-	 * leading to pressure on the active list is not a problem.
-	 */
-	refault_distance = (refault - eviction) & EVICTION_MASK;
-
-	/*
-	 * Compare the distance to the existing workingset size. We
-	 * don't activate pages that couldn't stay resident even if
-	 * all the memory was available to the workingset. Whether
-	 * workingset competition needs to consider anon or not depends
-	 * on having free swap space.
-	 */
-	active = lruvec_page_state(eviction_lruvec, NR_ACTIVE_FILE);
-	inactive_file = lruvec_page_state(eviction_lruvec, NR_INACTIVE_FILE);
-
-	if (mem_cgroup_get_nr_swap_pages(eviction_memcg) > 0) {
-		active += lruvec_page_state(eviction_lruvec,
-						     NR_ACTIVE_ANON);
-		inactive_anon = lruvec_page_state(eviction_lruvec,
-						  NR_INACTIVE_ANON);
-	} else {
-		inactive_anon = 0;
-	}
-
-	/*
-	 * When there are already enough active pages, be less aggressive
-	 * on reactivating pages, challenge an large set of established
-	 * active pages with one time refaulted page may not be a good idea.
-	 */
-	if (active >= inactive_anon + inactive_file)
-		return refault_distance < inactive_anon + inactive_file;
-	else
-		return refault_distance < active + (file ? inactive_anon : inactive_file);
+	return lru_test_refault(eviction_memcg, eviction_lruvec, eviction,
+				file, EVICTION_BITS, bucket_order);
 }
 
 /**
Signature · 1 line
2.41.0
#6 lore raw

RFC PATCH v3 3/6 workignset: simplify the initilization code

In reply to Kairui Song

From: Kairui Song <[email protected]>

Use the new introduced EVICTION_BITS to replace timestamp_bits, compiler should be able to optimize out the previous variable but this should make the code more clear and unified.

---
 mm/workingset.c | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)
mm/workingset.c +3 -5
index b0704cbfc667..278c3b9eb549 100644
@@ -772,7 +772,6 @@ static struct lock_class_key shadow_nodes_key;
 
 static int __init workingset_init(void)
 {
-	unsigned int timestamp_bits;
 	unsigned int max_order;
 	int ret;
 
@@ -784,12 +783,11 @@ static int __init workingset_init(void)
 	 * some more pages at runtime, so keep working with up to
 	 * double the initial memory by using totalram_pages as-is.
 	 */
-	timestamp_bits = BITS_PER_LONG - EVICTION_SHIFT;
 	max_order = fls_long(totalram_pages() - 1);
-	if (max_order > timestamp_bits)
-		bucket_order = max_order - timestamp_bits;
+	if (max_order > EVICTION_BITS)
+		bucket_order = max_order - EVICTION_BITS;
 	pr_info("workingset: timestamp_bits=%d max_order=%d bucket_order=%u\n",
-	       timestamp_bits, max_order, bucket_order);
+		EVICTION_BITS, max_order, bucket_order);
 
 	ret = prealloc_shrinker(&workingset_shadow_shrinker, "mm-shadow");
 	if (ret)
Signature · 1 line
2.41.0
#7 lore raw

RFC PATCH v3 4/6 workingset: simplify lru_gen_test_recent

In reply to Kairui Song

From: Kairui Song <[email protected]>

Simplify the code, move some common path into its caller, prepare for following commits.

---
 mm/workingset.c | 30 +++++++++++++-----------------
 1 file changed, 13 insertions(+), 17 deletions(-)
mm/workingset.c +13 -17
index 278c3b9eb549..87a16b6158e5 100644
@@ -323,42 +323,38 @@ static void *lru_gen_eviction(struct folio *folio)
  * Tests if the shadow entry is for a folio that was recently evicted.
  * Fills in @lruvec, @token, @workingset with the values unpacked from shadow.
  */
-static bool lru_gen_test_recent(void *shadow, bool file, struct lruvec **lruvec,
-				unsigned long *token, bool *workingset)
+static bool lru_gen_test_recent(struct lruvec *lruvec, bool file,
+				unsigned long token)
 {
-	int memcg_id;
 	unsigned long min_seq;
-	struct mem_cgroup *memcg;
-	struct pglist_data *pgdat;
 
-	unpack_shadow(shadow, &memcg_id, &pgdat, token, workingset);
-
-	memcg = mem_cgroup_from_id(memcg_id);
-	*lruvec = mem_cgroup_lruvec(memcg, pgdat);
-
-	min_seq = READ_ONCE((*lruvec)->lrugen.min_seq[file]);
-	return (*token >> LRU_REFS_WIDTH) == (min_seq & (EVICTION_MASK >> LRU_REFS_WIDTH));
+	min_seq = READ_ONCE(lruvec->lrugen.min_seq[file]);
+	return (token >> LRU_REFS_WIDTH) == (min_seq & (EVICTION_MASK >> LRU_REFS_WIDTH));
 }
 
 static void lru_gen_refault(struct folio *folio, void *shadow)
 {
+	int memcgid;
 	bool recent;
-	int hist, tier, refs;
 	bool workingset;
 	unsigned long token;
+	int hist, tier, refs;
 	struct lruvec *lruvec;
+	struct pglist_data *pgdat;
 	struct lru_gen_folio *lrugen;
 	int type = folio_is_file_lru(folio);
 	int delta = folio_nr_pages(folio);
 
 	rcu_read_lock();
 
-	recent = lru_gen_test_recent(shadow, type, &lruvec, &token, &workingset);
+	unpack_shadow(shadow, &memcgid, &pgdat, &token, &workingset);
+	lruvec = mem_cgroup_lruvec(mem_cgroup_from_id(memcgid), pgdat);
 	if (lruvec != folio_lruvec(folio))
 		goto unlock;
 
 	mod_lruvec_state(lruvec, WORKINGSET_REFAULT_BASE + type, delta);
 
+	recent = lru_gen_test_recent(lruvec, type, token);
 	if (!recent)
 		goto unlock;
 
@@ -485,9 +481,6 @@ bool workingset_test_recent(void *shadow, bool file, bool *workingset)
 	struct pglist_data *pgdat;
 	unsigned long eviction;
 
-	if (lru_gen_enabled())
-		return lru_gen_test_recent(shadow, file, &eviction_lruvec, &eviction, workingset);
-
 	unpack_shadow(shadow, &memcgid, &pgdat, &eviction, workingset);
 
 	/*
@@ -511,6 +504,9 @@ bool workingset_test_recent(void *shadow, bool file, bool *workingset)
 		return false;
 	eviction_lruvec = mem_cgroup_lruvec(eviction_memcg, pgdat);
 
+	if (lru_gen_enabled())
+		return lru_gen_test_recent(eviction_lruvec, file, eviction);
+
 	return lru_test_refault(eviction_memcg, eviction_lruvec, eviction,
 				file, EVICTION_BITS, bucket_order);
 }
Signature · 1 line
2.41.0
#8 lore raw

Re: [RFC PATCH v3 4/6] workingset: simplify lru_gen_test_recent

kernel test robot noticed the following build errors:

Hi Kairui,

[This is a private test report for your RFC patch.] kernel test robot noticed the following build errors:

[auto build test ERROR on linus/master]
[also build test ERROR on v6.6-rc2]
[cannot apply to akpm-mm/mm-everything next-20230920]
[If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Kairui-Song/workingset-simplify-and-use-a-more-intuitive-model/20230921-030515
base:   linus/master
patch link:    https://lore.kernel.org/r/20230920190244.16839-5-ryncsn%40gmail.com
patch subject: [RFC PATCH v3 4/6] workingset: simplify lru_gen_test_recent
config: powerpc-allnoconfig (https://download.01.org/0day-ci/archive/20230921/[email protected]/config)
compiler: powerpc-linux-gcc (GCC) 13.2.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20230921/[email protected]/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/

All errors (new ones prefixed by >>):

mm/workingset.c:415:6: warning: no previous prototype for 'workingset_age_nonresident' [-Wmissing-prototypes]
415 | void workingset_age_nonresident(struct lruvec *lruvec, unsigned long nr_pages)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
mm/workingset.c: In function 'workingset_test_recent':
mm/workingset.c:508:67: warning: passing argument 3 of 'lru_gen_test_recent' makes pointer from integer without a cast [-Wint-conversion]

     508 |                 return lru_gen_test_recent(eviction_lruvec, file, eviction);
         |                                                                   ^~~~~~~~
         |                                                                   |
         |                                                                   long unsigned int
   mm/workingset.c:393:74: note: expected 'struct lruvec **' but argument is of type 'long unsigned int'
     393 | static bool lru_gen_test_recent(void *shadow, bool file, struct lruvec **lruvec,
         |                                                          ~~~~~~~~~~~~~~~~^~~~~~
>> mm/workingset.c:508:24: error: too few arguments to function 'lru_gen_test_recent'
     508 |                 return lru_gen_test_recent(eviction_lruvec, file, eviction);
         |                        ^~~~~~~~~~~~~~~~~~~

mm/workingset.c:393:13: note: declared here
393 | static bool lru_gen_test_recent(void *shadow, bool file, struct lruvec **lruvec,
| ^~~~~~~~~~~~~~~~~~~

vim +/lru_gen_test_recent +508 mm/workingset.c

464
465 /**
466 * workingset_test_recent - tests if the shadow entry is for a folio that was
467 * recently evicted. Also fills in @workingset with the value unpacked from
468 * shadow.
469 * @shadow: the shadow entry to be tested.
470 * @file: whether the corresponding folio is from the file lru.
471 * @workingset: where the workingset value unpacked from shadow should
472 * be stored.
473 *
474 * Return: true if the shadow is for a recently evicted folio; false otherwise.
475 */
476 bool workingset_test_recent(void *shadow, bool file, bool *workingset)
477 {
478 struct mem_cgroup *eviction_memcg;
479 struct lruvec *eviction_lruvec;
480 int memcgid;
481 struct pglist_data *pgdat;
482 unsigned long eviction;
483
484 unpack_shadow(shadow, &memcgid, &pgdat, &eviction, workingset);
485
486 /*
487 * Look up the memcg associated with the stored ID. It might
488 * have been deleted since the folio's eviction.
489 *
490 * Note that in rare events the ID could have been recycled
491 * for a new cgroup that refaults a shared folio. This is
492 * impossible to tell from the available data. However, this
493 * should be a rare and limited disturbance, and activations
494 * are always speculative anyway. Ultimately, it's the aging
495 * algorithm's job to shake out the minimum access frequency
496 * for the active cache.
497 *
498 * XXX: On !CONFIG_MEMCG, this will always return NULL; it
499 * would be better if the root_mem_cgroup existed in all
500 * configurations instead.
501 */
502 eviction_memcg = mem_cgroup_from_id(memcgid);
503 if (!mem_cgroup_disabled() && !eviction_memcg)
504 return false;
505 eviction_lruvec = mem_cgroup_lruvec(eviction_memcg, pgdat);
506
507 if (lru_gen_enabled())

 > 508			return lru_gen_test_recent(eviction_lruvec, file, eviction);

509
510 return lru_test_refault(eviction_memcg, eviction_lruvec, eviction,
511 file, EVICTION_BITS, bucket_order);
512 }
513

Signature · 2 lines
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
#9 lore raw

RFC PATCH v3 5/6 mm, lru_gen: convert avg_total and avg_refaulted to atomic

In reply to Kairui Song

From: Kairui Song <[email protected]>

No feature change, prepare for later patch.

---
 include/linux/mmzone.h |  4 ++--
 mm/vmscan.c            | 16 ++++++++--------
 2 files changed, 10 insertions(+), 10 deletions(-)
include/linux/mmzone.h +2 -2
index 4106fbc5b4b3..d944987b67d3 100644
@@ -425,9 +425,9 @@ struct lru_gen_folio {
 	/* the multi-gen LRU sizes, eventually consistent */
 	long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
 	/* the exponential moving average of refaulted */
-	unsigned long avg_refaulted[ANON_AND_FILE][MAX_NR_TIERS];
+	atomic_long_t avg_refaulted[ANON_AND_FILE][MAX_NR_TIERS];
 	/* the exponential moving average of evicted+protected */
-	unsigned long avg_total[ANON_AND_FILE][MAX_NR_TIERS];
+	atomic_long_t avg_total[ANON_AND_FILE][MAX_NR_TIERS];
 	/* the first tier doesn't need protection, hence the minus one */
 	unsigned long protected[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS - 1];
 	/* can be modified without holding the LRU lock */
mm/vmscan.c +8 -8
index 3f4de75e5186..82acc1934c86 100644
@@ -3705,9 +3705,9 @@ static void read_ctrl_pos(struct lruvec *lruvec, int type, int tier, int gain,
 	struct lru_gen_folio *lrugen = &lruvec->lrugen;
 	int hist = lru_hist_from_seq(lrugen->min_seq[type]);
 
-	pos->refaulted = lrugen->avg_refaulted[type][tier] +
+	pos->refaulted = atomic_long_read(&lrugen->avg_refaulted[type][tier]) +
 			 atomic_long_read(&lrugen->refaulted[hist][type][tier]);
-	pos->total = lrugen->avg_total[type][tier] +
+	pos->total = atomic_long_read(&lrugen->avg_total[type][tier]) +
 		     atomic_long_read(&lrugen->evicted[hist][type][tier]);
 	if (tier)
 		pos->total += lrugen->protected[hist][type][tier - 1];
@@ -3732,15 +3732,15 @@ static void reset_ctrl_pos(struct lruvec *lruvec, int type, bool carryover)
 		if (carryover) {
 			unsigned long sum;
 
-			sum = lrugen->avg_refaulted[type][tier] +
+			sum = atomic_long_read(&lrugen->avg_refaulted[type][tier]) +
 			      atomic_long_read(&lrugen->refaulted[hist][type][tier]);
-			WRITE_ONCE(lrugen->avg_refaulted[type][tier], sum / 2);
+			atomic_long_set(&lrugen->avg_refaulted[type][tier], sum / 2);
 
-			sum = lrugen->avg_total[type][tier] +
+			sum = atomic_long_read(&lrugen->avg_total[type][tier]) +
 			      atomic_long_read(&lrugen->evicted[hist][type][tier]);
 			if (tier)
 				sum += lrugen->protected[hist][type][tier - 1];
-			WRITE_ONCE(lrugen->avg_total[type][tier], sum / 2);
+			atomic_long_set(&lrugen->avg_total[type][tier], sum / 2);
 		}
 
 		if (clear) {
@@ -5885,8 +5885,8 @@ static void lru_gen_seq_show_full(struct seq_file *m, struct lruvec *lruvec,
 
 			if (seq == max_seq) {
 				s = "RT ";
-				n[0] = READ_ONCE(lrugen->avg_refaulted[type][tier]);
-				n[1] = READ_ONCE(lrugen->avg_total[type][tier]);
+				n[0] = atomic_long_read(&lrugen->avg_refaulted[type][tier]);
+				n[1] = atomic_long_read(&lrugen->avg_total[type][tier]);
 			} else if (seq == min_seq[type] || NR_HIST_GENS > 1) {
 				s = "rep";
 				n[0] = atomic_long_read(&lrugen->refaulted[hist][type][tier]);
Signature · 1 line
2.41.0
#10 lore raw

RFC PATCH v3 6/6 workingset, lru_gen: apply refault-distance based re-activation

In reply to Kairui Song

From: Kairui Song <[email protected]>

I noticed MGLRU not working very well on certain workflows, which is observed on some heavily stressed databases. That is when the file page workingset size exceeds total memory, and the access distance (the left-shift time of a page before it gets activated, considering LRU starts from right) of file pages also larger than total memory. All file pages are stuck on the oldest generation and getting read-in then evicted permutably. Despite anon pages being idle, they never get aged. PID controller didn't kickin until there are some minor access pattern changes. And file pages are not promoted or reused.

Even though the memory can't cover the whole workingset, the refault-distance based re-activation can help hold part of the workingset in-memory to help reduce the IO workload significantly.

So apply it for MGLRU as well. The updated refault-distance model fits well for MGLRU in most cases, if we just consider the last two generation as the inactive LRU and the first two generations as active LRU.

Some adjustment is done to fit the logic better, also make the refault-distance contributed to page tiering and PID refault detection of MGLRU:

- If a tier-0 page have a qualified refault-distance, just promote it to higher tier, send it to second oldest gen.
- If a tier >= 1 page have a qualified refault-distance, mark it as active and send it to youngest gen.
- Increase the reference of every page that have a qualified refault-distance and increase the PID countroled refault rate of the updated tier, in hope similar paged will be protected next time upon eviction.

NOTE: This also changed the meaning of workingset_* fields in /proc/vmstat, workingset_activate_* now stands for the pages reactivated or promoted by refault distance checking, workingset_restore_* now stands for all pages promoted by any reason.

Following benchmark showed 5x improvement. To simulate the optimized workflow, I setup a 3-replicated mongodb cluster, each in a different cgroup, using 5 gb of wiretiger cache and 10g of oplog, on a 32G VM with no limit set. The benchmark is done using
https://github.com/apavlo/py-tpcc.git, modified to run STOCK_LEVEL query only, for simulating slow query and get a stable result.

Test is done on an EPYC 7K62 with 32G RAM with SATA SSD:

- Before (with ZRAM enabled, the result won't change whether any kind of swap is on or not):

$ tpcc.py --config=mongodb.config mongodb --duration=900 --warehouses=500 --clients=30
==================================================================
Execution Results after 919 seconds
------------------------------------------------------------------
                  Executed        Time (µs)       Rate
  STOCK_LEVEL     577             27584645283.7   0.02 txn/s
------------------------------------------------------------------
  TOTAL           577             27584645283.7   0.02 txn/s
$ cat /proc/vmstat | grep workingset
workingset_nodes 47860
workingset_refault_anon 0
workingset_refault_file 23498953
workingset_activate_anon 0
workingset_activate_file 23487840
workingset_restore_anon 0
workingset_restore_file 18553646
workingset_nodereclaim 768
$ free -m
              total        used        free      shared  buff/cache   available
Mem:          31849        6829         790          23       24229       24542
Swap:         31848           0       31848

- Patched: (with ZRAM enabled):

$ tpcc.py --config=mongodb.config mongodb --duration=900 --warehouses=500 --clients=30
==================================================================
Execution Results after 905 seconds
------------------------------------------------------------------
                  Executed        Time (µs)       Rate
  STOCK_LEVEL     2542            27121571486.2   0.09 txn/s
------------------------------------------------------------------
  TOTAL           2542            27121571486.2   0.09 txn/s
$ cat /proc/vmstat | grep working
workingset_nodes 70358
workingset_refault_anon 16853
workingset_refault_file 22693601
workingset_activate_anon 10099
workingset_activate_file 8565519
workingset_restore_anon 10127
workingset_restore_file 8566053
workingset_nodereclaim 9801
$ free -m
              total        used        free      shared  buff/cache   available
Mem:          31849        7093         283           4       24472       24289
Swap:         31848        1652       30196

The performance is 5x times better than before, and the idle anon pages now can get swapped out as expected. The result is also better with lower test stress, testing with lower stress also shows a improvement.

I also checked the benchmark with memtier/memcached and fio, using similar setup as in commit ac35a4902374 but scaled down to fit in my test environment:

  memtier test (16G ramdisk as swap, 4G memcg limit, VM on a EPYC 7K62):
  memcached -u nobody -m 16384 -s /tmp/memcached.socket -a 0766 \
    -t 16 -B binary &
  memtier_benchmark -S /tmp/memcached.socket -P memcache_binary -n allkeys\
    --key-minimum=1 --key-maximum=36000000 --key-pattern=P:P -c 1 \
    -t 16 --ratio 1:0 --pipeline 8 -d 600 -x 6

  fio test 1 (16G ramdisk, 4G memcg limit, VM on a EPYC 7K62):
  fio -name=mglru --numjobs=16 --directory=/mnt --size=1000m \
    --buffered=1 --ioengine=io_uring --iodepth=128 \
    --iodepth_batch_submit=32 --iodepth_batch_complete=32 \
    --rw=randread --random_distribution=zipf:1.2 --norandommap \
    --time_based --ramp_time=10m --runtime=5m --group_reporting

  fio test 2 (16G ramdisk, 2G memcg limit, VM on a EPYC 7K62):
  fio -name=mglru --numjobs=16 --directory=/mnt --size=1000m \
    --buffered=1 --ioengine=io_uring --iodepth=128 \
    --iodepth_batch_submit=32 --iodepth_batch_complete=32 \
    --rw=randread --random_distribution=zipf:1.2 --norandommap \
    --time_based --ramp_time=10m --runtime=5m --group_reporting

  mysql test (15G buffer pool with 16G memcg limit, VM on a EPYC 7K62):
    sysbench /usr/share/sysbench/oltp_read_only.lua <auth and db params> \
      --tables=48 --table-size=2000000 --threads=16 --time=1800 run

Before this patch:
memtier: 37794.71 op/s
fio 1: 6327.3k iops
fio 2: 5697.6k iops
mysql: 146104.98 qps

After this patch:
memtier: 37792.61 op/s
fio 1: 6583.3k iops
fio 2: 5929.2k iops
mysql: 146055.88 qps

There is no regression on other tests so far, and a performance gain is observed on file page heavy tasks.

---
 mm/vmscan.c     |  20 +++++---
 mm/workingset.c | 130 +++++++++++++++++++++++++++++++-----------------
 2 files changed, 95 insertions(+), 55 deletions(-)
mm/vmscan.c +12 -8
index 82acc1934c86..c7745b22cc0b 100644
@@ -3730,17 +3730,21 @@ static void reset_ctrl_pos(struct lruvec *lruvec, int type, bool carryover)
 
 	for (tier = 0; tier < MAX_NR_TIERS; tier++) {
 		if (carryover) {
-			unsigned long sum;
+			unsigned long refaulted, total;
 
-			sum = atomic_long_read(&lrugen->avg_refaulted[type][tier]) +
-			      atomic_long_read(&lrugen->refaulted[hist][type][tier]);
-			atomic_long_set(&lrugen->avg_refaulted[type][tier], sum / 2);
+			refaulted = atomic_long_read(&lrugen->avg_refaulted[type][tier]) +
+				atomic_long_read(&lrugen->refaulted[hist][type][tier]);
 
-			sum = atomic_long_read(&lrugen->avg_total[type][tier]) +
-			      atomic_long_read(&lrugen->evicted[hist][type][tier]);
+			total = atomic_long_read(&lrugen->avg_total[type][tier]) +
+				atomic_long_read(&lrugen->evicted[hist][type][tier]);
 			if (tier)
-				sum += lrugen->protected[hist][type][tier - 1];
-			atomic_long_set(&lrugen->avg_total[type][tier], sum / 2);
+				total += lrugen->protected[hist][type][tier - 1];
+
+			/* total could be less than refaulted, see lru_gen_refault */
+			total = max(total, refaulted);
+
+			atomic_long_set(&lrugen->avg_refaulted[type][tier], refaulted / 2);
+			atomic_long_set(&lrugen->avg_total[type][tier], total / 2);
 		}
 
 		if (clear) {
mm/workingset.c +83 -47

222 lines not on this page · raw message

Signature · 1 line
2.41.0