The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Johannes Weiner <hannes@cmpxchg.org>
To: Usama Arif <usama.arif@linux.dev>
Cc: Andrew Morton <akpm@linux-foundation.org>,
	david@kernel.org, ljs@kernel.org, liam@infradead.org,
	vbabka@kernel.org, rppt@kernel.org, surenb@google.com,
	mhocko@suse.com, kasong@tencent.com, qi.zheng@linux.dev,
	shakeel.butt@linux.dev, axelrasmussen@google.com,
	yuanchu@google.com, weixugc@google.com, chrisl@kernel.org,
	nphamcs@gmail.com, baoquan.he@linux.dev, youngjun.park@lge.com,
	roman.gushchin@linux.dev, muchun.song@linux.dev,
	linux-mm@kvack.org, linux-kernel@vger.kernel.org,
	cgroups@vger.kernel.org, rientjes@google.com,
	kernel-team@meta.com
Subject: Re: [PATCH v3 2/2] mm/vmscan: reduce lru_lock contention via vmstat-derived scan-balance cost
Date: Fri, 17 Jul 2026 23:08:08 +0200	[thread overview]
Message-ID: <20260717210808.GI6843@cmpxchg.org> (raw)
In-Reply-To: <20260717135807.3476029-3-usama.arif@linux.dev>

On Fri, Jul 17, 2026 at 06:57:32AM -0700, Usama Arif wrote:
> @@ -2303,12 +2301,63 @@ static void prepare_scan_control(pg_data_t *pgdat, struct scan_control *sc)
>  	mem_cgroup_flush_stats_ratelimited(sc->target_mem_cgroup);
>  
>  	/*
> -	 * Determine the scan balance between anon and file LRUs.
> +	 * Determine the scan balance between anon and file LRUs from per-LRU
> +	 * vmstat counters. The raw cost per side is:
> +	 *
> +	 *	PGROTATE	   - reclaim-driven rotations, bumped from both
> +	 *			     shrink_inactive_list and shrink_active_list
> +	 *			     (CPU work).
> +	 *	NR_VMSCAN_WRITE    - reclaim-driven anon pageout IO.
> +	 *	WORKINGSET_RESTORE - refaults of previously-workingset pages.
> +	 *
> +	 * The two IO terms are weighted by SWAP_CLUSTER_MAX to reflect the
> +	 * higher cost of an IO over a rotation.
> +	 *
> +	 * Reads are lock-free per-cpu sum collations, rstat-aggregated up
> +	 * the memcg hierarchy by mem_cgroup_flush_stats_ratelimited() above.
> +	 * Use lruvec_page_state_monotonic() so the unsigned subtraction
> +	 * `now - prev_cost[f]` yields the correct delta across a signed-long
> +	 * wraparound of the underlying counter (a real hazard on 32-bit that
> +	 * the clamp in lruvec_page_state() would otherwise turn into a huge
> +	 * spurious delta).
> +	 *
> +	 * The delta against prev_cost is folded into cost_accum, which is
> +	 * halved on both sides until their sum is within lrusize/4.
> +	 * cost_lock serialises concurrent reclaimers in the same memcg+node.

IMO that's a lot of describing what the code does. Why not stick
closer to the original comments?

>  	 */
> -	spin_lock_irq(&target_lruvec->lru_lock);
> -	sc->anon_cost = target_lruvec->anon_cost;
> -	sc->file_cost = target_lruvec->file_cost;
> -	spin_unlock_irq(&target_lruvec->lru_lock);
> +	spin_lock(&target_lruvec->cost_lock);
> +	for (int f = 0; f <= 1; f++) {
> +		unsigned long now, delta;
> +
> +		now = lruvec_page_state_monotonic(target_lruvec, PGROTATE_ANON + f) +
> +		      lruvec_page_state_monotonic(target_lruvec,
> +						  WORKINGSET_RESTORE_BASE + f) *
> +				SWAP_CLUSTER_MAX;
> +		if (f == WORKINGSET_ANON)
> +			now += lruvec_page_state_monotonic(target_lruvec,
> +							   NR_VMSCAN_WRITE) *
> +				SWAP_CLUSTER_MAX;

It's hard to prove overflow behavior is correct. I would keep the
delta extraction dead simple, then do the weight math on the delta.

> +		delta = now - target_lruvec->prev_cost[f];
> +		target_lruvec->prev_cost[f] = now;
> +		target_lruvec->cost_accum[f] += delta;
> +	}
> +	unsigned long lrusize =
> +		lruvec_page_state(target_lruvec, NR_INACTIVE_ANON) +
> +		lruvec_page_state(target_lruvec, NR_ACTIVE_ANON) +
> +		lruvec_page_state(target_lruvec, NR_INACTIVE_FILE) +
> +		lruvec_page_state(target_lruvec, NR_ACTIVE_FILE);
> +	unsigned long cost_limit = lrusize / 4;
> +
> +	while (target_lruvec->cost_accum[WORKINGSET_ANON] > cost_limit ||
> +	       target_lruvec->cost_accum[WORKINGSET_FILE] > cost_limit ||
> +	       target_lruvec->cost_accum[WORKINGSET_ANON] +
> +	       target_lruvec->cost_accum[WORKINGSET_FILE] > cost_limit) {
> +		target_lruvec->cost_accum[WORKINGSET_ANON] /= 2;
> +		target_lruvec->cost_accum[WORKINGSET_FILE] /= 2;

Why do you need to check them individually? Between reclaim cycles,
there are no scans->rotations. And I don't see how you could get
refault events several times the size of the LRU, let alone in excess
of ULONG_MAX.

> +	}
> +	sc->anon_cost = target_lruvec->cost_accum[WORKINGSET_ANON];
> +	sc->file_cost = target_lruvec->cost_accum[WORKINGSET_FILE];
> +	spin_unlock(&target_lruvec->cost_lock);

I realize these long descriptor names make it hard, but I think this
can be cleaned up a bit and written in a more idiomatic way.

How about this (totally untested):

struct lru_cost {
	unsigned long count;
	unsigned long last_rotated;
	unsigned long last_io;
};

struct lruvec {
	struct lru_cost cost[2];
};

	struct lru_cost *anon_cost, *file_cost;
	unsigned long lrusize;
	
	/*
	 * Determine the scan balance between anon and file LRUs.
	 *
	 * The cost model is based on rotations, refaults and
	 * reclaim-driven writes (anon only) on each side.
	 *
	 * These event counters are monotonic, so each reclaim cycle
	 * the delta since the last scan is extracted and incorporated
	 * into a decaying average. This ensures currency, as workloads
	 * change over time, and avoids overflow in the calculations.
	 */
	spin_lock_irq(&target_lruvec->cost_lock);

	for (file = 0; file <= 1; file++) {
		struct lru_cost *cost = &target_lruvec->cost[file];
		unsigned long rotated, io, nr_rotated, nr_io;

		rotated = lruvec_page_state_monotonic(target_lruvec,
						PGROTATE_ANON + file);
		io = lruvec_page_state_monotonic(target_lruvec,
						WORKINGSET_RESTORE_BASE + file);
		if (!file)
			io += lruvec_page_state_monotonic(target_lruvec,
						NR_VMSCAN_WRITE);

		nr_rotated = rotated - cost->last_rotated;
		nr_io = io - cost->last_io;

		/*
		 * Reflect the relative cost of incurring IO and spending CPU
		 * time on rotations. This doesn't attempt to make a precise
		 * comparison, it just says: if reloads are about comparable
		 * between the LRU lists, or rotations are overwhelmingly
		 * different between them, adjust scan balance for CPU work.
		 */
		cost->count += nr_io * SWAP_CLUSTER_MAX + nr_rotated;

		cost->last_rotated = rotated;
		cost->last_io = io;
	}

	anon_cost = &target_lruvec->cost[WORKINGSET_ANON];
	file_cost = &target_lruvec->cost[WORKINGSET_FILE];

	lrusize = lruvec_page_state(target_lruvec, NR_INACTIVE_ANON) +
		  lruvec_page_state(target_lruvec, NR_ACTIVE_ANON) +
		  lruvec_page_state(target_lruvec, NR_INACTIVE_FILE) +
		  lruvec_page_state(target_lruvec, NR_ACTIVE_FILE);

	while (anon_cost->count + file_cost->count > lrusize / 4) {
		anon_cost->count /= 2;
		file_cost->count /= 2;
	}

	sc->anon_cost = anon_cost->count;
	sc->file_cost = file_cost->count;

	spin_unlock(&target_lruvec->cost_lock);

      reply	other threads:[~2026-07-17 21:08 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-17 13:57 [PATCH v3 0/2] mm/vmscan: reduce lru_lock contention via vmstat-derived scan-balance cost Usama Arif
2026-07-17 13:57 ` [PATCH v3 1/2] mm/vmstat, mm/memcontrol: add _monotonic vmstat readers Usama Arif
2026-07-17 13:57 ` [PATCH v3 2/2] mm/vmscan: reduce lru_lock contention via vmstat-derived scan-balance cost Usama Arif
2026-07-17 21:08   ` Johannes Weiner [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260717210808.GI6843@cmpxchg.org \
    --to=hannes@cmpxchg.org \
    --cc=akpm@linux-foundation.org \
    --cc=axelrasmussen@google.com \
    --cc=baoquan.he@linux.dev \
    --cc=cgroups@vger.kernel.org \
    --cc=chrisl@kernel.org \
    --cc=david@kernel.org \
    --cc=kasong@tencent.com \
    --cc=kernel-team@meta.com \
    --cc=liam@infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=ljs@kernel.org \
    --cc=mhocko@suse.com \
    --cc=muchun.song@linux.dev \
    --cc=nphamcs@gmail.com \
    --cc=qi.zheng@linux.dev \
    --cc=rientjes@google.com \
    --cc=roman.gushchin@linux.dev \
    --cc=rppt@kernel.org \
    --cc=shakeel.butt@linux.dev \
    --cc=surenb@google.com \
    --cc=usama.arif@linux.dev \
    --cc=vbabka@kernel.org \
    --cc=weixugc@google.com \
    --cc=youngjun.park@lge.com \
    --cc=yuanchu@google.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox