All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.