memcg: trim the per-cpu charge stock instead of draining it
Subsystems (3)
Shakeel Butt
maintainerPATCH 1/1 memcg: trim the per-cpu charge stock instead of draining it
Joy reported that an application generating a request/response traffic pattern spends 44.6% to 57.0% of CPU in the memcg charge/uncharge path for a range of message sizes, against 0.27% to 0.71% outside that range. Running from the root memcg, where socket memory accounting is skipped, recovers the performance.
Tracing the charge path showed that the application generates a pattern where the write syscall charges one page and the read syscall uncharges two pages on the same CPU. This hits a corner case in the memcg percpu stock code that thrashes the stock continuously.
In the memcg percpu stock code, MEMCG_CHARGE_BATCH (64) is both the high watermark and the emptying target, i.e. on a request to charge one page the kernel charges MEMCG_CHARGE_BATCH pages and caches (MEMCG_CHARGE_BATCH - 1) of them in the percpu stock. The following uncharge of 2 pages takes the cached count to (MEMCG_CHARGE_BATCH + 1), and refill_stock() then empties the cache completely. With such a pattern the percpu stock becomes completely ineffective.
Instead of a single boundary point for charges, use the technique the page allocator uses for its own percpu caches, which keeps the watermark and the emptying target apart: nr_pcp_free() frees between batch and high - batch pages, leaving at least pcp->batch on the list. Add a high watermark MEMCG_STOCK_HIGH and, once the cached count goes over it, return only the pages above MEMCG_STOCK_LOW. The watermarks are MEMCG_CHARGE_BATCH apart, so a page_counter update still covers a full batch. Peak cached pages per memcg grows from 64 to 96, the same high-versus-batch tradeoff the page allocator makes.
--- mm/memcontrol.c | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-)
mm/memcontrol.c +25 -6
@@ -2048,6 +2048,21 @@ void mem_cgroup_print_oom_group(struct mem_cgroup *memcg)* nr_pages in a single cacheline. This may change in future. */ #define NR_MEMCG_STOCK 7 + +/* +* Watermarks for a charge stock slot, in the spirit of pcp->high and +* pcp->batch: MEMCG_STOCK_HIGH is the high watermark at which a slot is +* trimmed, and it is trimmed down to MEMCG_STOCK_LOW rather than emptied. +* +* Using MEMCG_CHARGE_BATCH as both high watermark and emptying target +* thrashes: charging one page stocks 63, an uncharge of 2 takes the count +* to 65 and empties the slot, and the next charge misses. The watermarks +* are MEMCG_CHARGE_BATCH apart, so a page_counter update still covers a +* full batch. +*/ +#define MEMCG_STOCK_LOW (MEMCG_CHARGE_BATCH / 2) +#define MEMCG_STOCK_HIGH (MEMCG_STOCK_LOW + MEMCG_CHARGE_BATCH) +#define FLUSHING_CACHED_CHARGE 0struct memcg_stock_pcp {local_trylock_t lock;
@@ -2223,17 +2238,18 @@ static void refill_stock(struct mem_cgroup *memcg, unsigned int nr_pages) {struct memcg_stock_pcp *stock;struct mem_cgroup *cached; -uint8_t stock_pages; +unsigned int stock_pages;bool success = false;int empty_slot =-1 ;int i;/* -* For now limit MEMCG_CHARGE_BATCH to 127 and less. In future if we -* decide to increase it more than 127 then we will need more careful -* handling of nr_pages[] in struct memcg_stock_pcp. +* nr_pages[] is a uint8_t and a slot's count is capped at +* MEMCG_STOCK_HIGH. Raising MEMCG_CHARGE_BATCH beyond 127 would need +* more careful handling of nr_pages[] in struct memcg_stock_pcp. */ BUILD_BUG_ON (MEMCG_CHARGE_BATCH >S8_MAX ); +BUILD_BUG_ON (MEMCG_STOCK_HIGH >U8_MAX );VM_WARN_ON_ONCE (mem_cgroup_is_root (memcg));
@@ -2254,9 +2270,12 @@ static void refill_stock(struct mem_cgroup *memcg, unsigned int nr_pages) empty_slot = i;if (memcg ==READ_ONCE (stock->cached[i])) { stock_pages =READ_ONCE (stock->nr_pages[i]) + nr_pages; +if (stock_pages >MEMCG_STOCK_HIGH ) { +memcg_uncharge (memcg, + stock_pages -MEMCG_STOCK_LOW ); + stock_pages =MEMCG_STOCK_LOW ; + }WRITE_ONCE (stock->nr_pages[i], stock_pages); -if (stock_pages >MEMCG_CHARGE_BATCH ) -drain_stock (stock, i); success = true;break ; }
Signature · 1 line
2.53.0-Meta
Quoted 19 lines · Shakeel Butt
Joy reported that an application generating a request/response traffic pattern spends 44.6% to 57.0% of CPU in the memcg charge/uncharge path for a range of message sizes, against 0.27% to 0.71% outside that range. Running from the root memcg, where socket memory accounting is skipped, recovers the performance.
Tracing the charge path showed that the application generates a pattern where the write syscall charges one page and the read syscall uncharges two pages on the same CPU. This hits a corner case in the memcg percpu stock code that thrashes the stock continuously.
In the memcg percpu stock code,
MEMCG_CHARGE_BATCH(64) is both the high watermark and the emptying target, i.e. on a request to charge one page the kernel chargesMEMCG_CHARGE_BATCHpages and caches (MEMCG_CHARGE_BATCH- 1) of them in the percpu stock. The following uncharge of 2 pages takes the cached count to (MEMCG_CHARGE_BATCH+ 1), andrefill_stock()then empties the cache completely. With such a pattern the percpu stock becomes completely ineffective.
Quoted 9 lines
Instead of a single boundary point for charges, use the technique the page allocator uses for its own percpu caches, which keeps the watermark and the emptying target apart:
nr_pcp_free()frees between batch and high - batch pages, leaving at leastpcp->batchon the list. Add a high watermarkMEMCG_STOCK_HIGHand, once the cached count goes over it, return only the pages aboveMEMCG_STOCK_LOW. The watermarks areMEMCG_CHARGE_BATCHapart, so apage_counterupdate still covers a full batch. Peak cached pages per memcg grows from 64 to 96, the same high-versus-batch tradeoff the page allocator makes.
The idea is sound. I would just not increase the overall stock size in the same patch. Fine tuning can be done independently and ideally with some numbers.
Would it make sense to start with MEMCG_STOCK_HIGH := MEMCG_CHARGE_BATCH and MEMCG_CHARGE_BATCH := MEMCG_CHARGE_BATCH / 2. That would preserve the maximum stock size while preventing all or nothing behavior which is indeed suboptimal and pushing charging path to a slower path way too aggressively.
WDYT?
Quoted 72 lines
Reported-by Joy Chaoyue Xiong <[email protected]>Signed-off-by Shakeel Butt <[email protected]>--- mm/memcontrol.c | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-)mm/memcontrol.c +25
-6@@ -2048,6 +2048,21 @@ void mem_cgroup_print_oom_group(struct mem_cgroup *memcg)* nr_pages in a single cacheline. This may change in future. */ #define NR_MEMCG_STOCK 7 + +/* +* Watermarks for a charge stock slot, in the spirit of pcp->high and +* pcp->batch: MEMCG_STOCK_HIGH is the high watermark at which a slot is +* trimmed, and it is trimmed down to MEMCG_STOCK_LOW rather than emptied. +* +* Using MEMCG_CHARGE_BATCH as both high watermark and emptying target +* thrashes: charging one page stocks 63, an uncharge of 2 takes the count +* to 65 and empties the slot, and the next charge misses. The watermarks +* are MEMCG_CHARGE_BATCH apart, so a page_counter update still covers a +* full batch. +*/ +#define MEMCG_STOCK_LOW (MEMCG_CHARGE_BATCH / 2) +#define MEMCG_STOCK_HIGH (MEMCG_STOCK_LOW + MEMCG_CHARGE_BATCH) +#define FLUSHING_CACHED_CHARGE 0struct memcg_stock_pcp {local_trylock_t lock;@@ -2223,17 +2238,18 @@ static void refill_stock(struct mem_cgroup *memcg, unsigned int nr_pages) {struct memcg_stock_pcp *stock;struct mem_cgroup *cached; -uint8_t stock_pages; +unsigned int stock_pages;bool success = false;int empty_slot =-1 ;int i;/* -* For now limit MEMCG_CHARGE_BATCH to 127 and less. In future if we -* decide to increase it more than 127 then we will need more careful -* handling of nr_pages[] in struct memcg_stock_pcp. +* nr_pages[] is a uint8_t and a slot's count is capped at +* MEMCG_STOCK_HIGH. Raising MEMCG_CHARGE_BATCH beyond 127 would need +* more careful handling of nr_pages[] in struct memcg_stock_pcp. */ BUILD_BUG_ON (MEMCG_CHARGE_BATCH >S8_MAX ); +BUILD_BUG_ON (MEMCG_STOCK_HIGH >U8_MAX );VM_WARN_ON_ONCE (mem_cgroup_is_root (memcg));@@ -2254,9 +2270,12 @@ static void refill_stock(struct mem_cgroup *memcg, unsigned int nr_pages) empty_slot = i;if (memcg ==READ_ONCE (stock->cached[i])) { stock_pages =READ_ONCE (stock->nr_pages[i]) + nr_pages; +if (stock_pages >MEMCG_STOCK_HIGH ) { +memcg_uncharge (memcg, + stock_pages -MEMCG_STOCK_LOW ); + stock_pages =MEMCG_STOCK_LOW ; + }WRITE_ONCE (stock->nr_pages[i], stock_pages); -if (stock_pages >MEMCG_CHARGE_BATCH ) -drain_stock (stock, i); success = true;break ; }Signature · 1 line
2.53.0-Meta
Signature · 2 lines
Michal Hocko SUSE Labs
Shakeel Butt
maintainerQuoted 39 lines · Michal Hocko
Quoted 19 lines · Shakeel Butt
Joy reported that an application generating a request/response traffic pattern spends 44.6% to 57.0% of CPU in the memcg charge/uncharge path for a range of message sizes, against 0.27% to 0.71% outside that range. Running from the root memcg, where socket memory accounting is skipped, recovers the performance.
Tracing the charge path showed that the application generates a pattern where the write syscall charges one page and the read syscall uncharges two pages on the same CPU. This hits a corner case in the memcg percpu stock code that thrashes the stock continuously.
In the memcg percpu stock code,
MEMCG_CHARGE_BATCH(64) is both the high watermark and the emptying target, i.e. on a request to charge one page the kernel chargesMEMCG_CHARGE_BATCHpages and caches (MEMCG_CHARGE_BATCH- 1) of them in the percpu stock. The following uncharge of 2 pages takes the cached count to (MEMCG_CHARGE_BATCH+ 1), andrefill_stock()then empties the cache completely. With such a pattern the percpu stock becomes completely ineffective.Quoted 9 lines
Instead of a single boundary point for charges, use the technique the page allocator uses for its own percpu caches, which keeps the watermark and the emptying target apart:
nr_pcp_free()frees between batch and high - batch pages, leaving at leastpcp->batchon the list. Add a high watermarkMEMCG_STOCK_HIGHand, once the cached count goes over it, return only the pages aboveMEMCG_STOCK_LOW. The watermarks areMEMCG_CHARGE_BATCHapart, so apage_counterupdate still covers a full batch. Peak cached pages per memcg grows from 64 to 96, the same high-versus-batch tradeoff the page allocator makes.The idea is sound. I would just not increase the overall stock size in the same patch. Fine tuning can be done independently and ideally with some numbers.
Would it make sense to start withMEMCG_STOCK_HIGH:=MEMCG_CHARGE_BATCHandMEMCG_CHARGE_BATCH:=MEMCG_CHARGE_BATCH/ 2. That would preserve the maximum stock size while preventing all or nothing behavior which is indeed suboptimal and pushing charging path to a slower path way too aggressively.
Quoted 1 line
WDYT?
Yes, this makes sense. Let me run the experiment with that workload to make sure the newer number works and resend the patch.
Thanks for the review.
Quoted 12 lines · On Tue, 18 Aug 2026 12:03:07 +0200 Michal Hocko <[email protected]> wrote:
Quoted 11 lines · Shakeel Butt
Joy reported that an application generating a request/response traffic pattern spends 44.6% to 57.0% of CPU in the memcg charge/uncharge path for a range of message sizes, against 0.27% to 0.71% outside that range. Running from the root memcg, where socket memory accounting is skipped, recovers the performance.
Tracing the charge path showed that the application generates a pattern where the write syscall charges one page and the read syscall uncharges two pages on the same CPU. This hits a corner case in the memcg percpu stock code that thrashes the stock continuously.
Quoted 7 lines
Quoted 7 lines
In the memcg percpu stock code,
MEMCG_CHARGE_BATCH(64) is both the high watermark and the emptying target, i.e. on a request to charge one page the kernel chargesMEMCG_CHARGE_BATCHpages and caches (MEMCG_CHARGE_BATCH- 1) of them in the percpu stock. The following uncharge of 2 pages takes the cached count to (MEMCG_CHARGE_BATCH+ 1), andrefill_stock()then empties the cache completely. With such a pattern the percpu stock becomes completely ineffective.
To be honest I never really understood why returning the stock and making the nr_pages go over the batch size completely drained the stock. One theory that I had was that if one uncharging is indicative that there will be more future uncharges coming and we want to make room for those uncharges to deposit their stocks, but now that I'm looking through the code I'm pretty sure that sequential stock uncharges are not that common, since we use uncharge_gather to batch up the LRU batch uncharging area.
Quoted 10 lines
Quoted 9 lines
Instead of a single boundary point for charges, use the technique the page allocator uses for its own percpu caches, which keeps the watermark and the emptying target apart:
nr_pcp_free()frees between batch and high - batch pages, leaving at leastpcp->batchon the list. Add a high watermarkMEMCG_STOCK_HIGHand, once the cached count goes over it, return only the pages aboveMEMCG_STOCK_LOW. The watermarks areMEMCG_CHARGE_BATCHapart, so apage_counterupdate still covers a full batch. Peak cached pages per memcg grows from 64 to 96, the same high-versus-batch tradeoff the page allocator makes.
Quoted 8 lines
The idea is sound. I would just not increase the overall stock size in the same patch. Fine tuning can be done independently and ideally with some numbers.
Would it make sense to start withMEMCG_STOCK_HIGH:=MEMCG_CHARGE_BATCHandMEMCG_CHARGE_BATCH:=MEMCG_CHARGE_BATCH/ 2. That would preserve the maximum stock size while preventing all or nothing behavior which is indeed suboptimal and pushing charging path to a slower path way too aggressively.
I like this idea and I think you meant { MEMCG_CHARGE_LOW := MEMCG_CHARGE_BATCH / 2 } right? That way the only new change we reintroduce is to always drain to a level when we return too many pages to the stock.
Have a great day, Michal and Shakeel!
Joshua
Quoted 78 lines
WDYT?
Quoted 72 lines
Reported-by Joy Chaoyue Xiong <[email protected]>Signed-off-by Shakeel Butt <[email protected]>--- mm/memcontrol.c | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-)mm/memcontrol.c +25
-6@@ -2048,6 +2048,21 @@ void mem_cgroup_print_oom_group(struct mem_cgroup *memcg)* nr_pages in a single cacheline. This may change in future. */ #define NR_MEMCG_STOCK 7 + +/* +* Watermarks for a charge stock slot, in the spirit of pcp->high and +* pcp->batch: MEMCG_STOCK_HIGH is the high watermark at which a slot is +* trimmed, and it is trimmed down to MEMCG_STOCK_LOW rather than emptied. +* +* Using MEMCG_CHARGE_BATCH as both high watermark and emptying target +* thrashes: charging one page stocks 63, an uncharge of 2 takes the count +* to 65 and empties the slot, and the next charge misses. The watermarks +* are MEMCG_CHARGE_BATCH apart, so a page_counter update still covers a +* full batch. +*/ +#define MEMCG_STOCK_LOW (MEMCG_CHARGE_BATCH / 2) +#define MEMCG_STOCK_HIGH (MEMCG_STOCK_LOW + MEMCG_CHARGE_BATCH) +#define FLUSHING_CACHED_CHARGE 0struct memcg_stock_pcp {local_trylock_t lock;@@ -2223,17 +2238,18 @@ static void refill_stock(struct mem_cgroup *memcg, unsigned int nr_pages) {struct memcg_stock_pcp *stock;struct mem_cgroup *cached; -uint8_t stock_pages; +unsigned int stock_pages;bool success = false;int empty_slot =-1 ;int i;/* -* For now limit MEMCG_CHARGE_BATCH to 127 and less. In future if we -* decide to increase it more than 127 then we will need more careful -* handling of nr_pages[] in struct memcg_stock_pcp. +* nr_pages[] is a uint8_t and a slot's count is capped at +* MEMCG_STOCK_HIGH. Raising MEMCG_CHARGE_BATCH beyond 127 would need +* more careful handling of nr_pages[] in struct memcg_stock_pcp. */ BUILD_BUG_ON (MEMCG_CHARGE_BATCH >S8_MAX ); +BUILD_BUG_ON (MEMCG_STOCK_HIGH >U8_MAX );VM_WARN_ON_ONCE (mem_cgroup_is_root (memcg));@@ -2254,9 +2270,12 @@ static void refill_stock(struct mem_cgroup *memcg, unsigned int nr_pages) empty_slot = i;if (memcg ==READ_ONCE (stock->cached[i])) { stock_pages =READ_ONCE (stock->nr_pages[i]) + nr_pages; +if (stock_pages >MEMCG_STOCK_HIGH ) { +memcg_uncharge (memcg, + stock_pages -MEMCG_STOCK_LOW ); + stock_pages =MEMCG_STOCK_LOW ; + }WRITE_ONCE (stock->nr_pages[i], stock_pages); -if (stock_pages >MEMCG_CHARGE_BATCH ) -drain_stock (stock, i); success = true;break ; }Signature · 1 line
2.53.0-MetaSignature · 2 lines
Michal Hocko SUSE Labs
Quoted 10 lines · Joshua Hahn
Quoted 8 lines · On Tue, 18 Aug 2026 12:03:07 +0200 Michal Hocko <[email protected]> wrote:
The idea is sound. I would just not increase the overall stock size in the same patch. Fine tuning can be done independently and ideally with some numbers.
Would it make sense to start withMEMCG_STOCK_HIGH:=MEMCG_CHARGE_BATCHandMEMCG_CHARGE_BATCH:=MEMCG_CHARGE_BATCH/ 2. That would preserve the maximum stock size while preventing all or nothing behavior which is indeed suboptimal and pushing charging path to a slower path way too aggressively.
Quoted 2 lines
I like this idea and I think you meant {
MEMCG_CHARGE_LOW:=MEMCG_CHARGE_BATCH/ 2 } right?
Correct. Wrong auto-completion on my end.
Signature · 2 lines
Michal Hocko SUSE Labs