Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

memcg: multi-memcg percpu charge cache

Memory cgroup accounting is expensive and to reduce the cost, the kernel
maintains per-cpu charge cache for a single memcg. So, if a charge
request comes for a different memcg, the kernel will flush the old memcg's
charge cache and then charge the newer memcg a fixed amount (64 pages),
subtracts the charge request amount and stores the remaining in the
per-cpu charge cache for the newer memcg.

This mechanism is based on the assumption that the kernel, for locality,
keep a process on a CPU for long period of time and most of the charge
requests from that process will be served by that CPU's local charge
cache.

However this assumption breaks down for incoming network traffic in a
multi-tenant machine. We are in the process of running multiple workloads
on a single machine and if such workloads are network heavy, we are seeing
very high network memory accounting cost. We have observed multiple CPUs
spending almost 100% of their time in net_rx_action and almost all of that
time is spent in memcg accounting of the network traffic.

More precisely, net_rx_action is serving packets from multiple workloads
and is observing/serving mix of packets of these workloads. The memcg
switch of per-cpu cache is very expensive and we are observing a lot of
memcg switches on the machine. Almost all the time is being spent on
charging new memcg and flushing older memcg cache. So, definitely we need
per-cpu cache that support multiple memcgs for this scenario.

This patch implements a simple (and dumb) multiple memcg percpu charge
cache. Actually we started with more sophisticated LRU based approach but
the dumb one was always better than the sophisticated one by 1% to 3%, so
going with the simple approach.

Some of the design choices are:

1. Fit all caches memcgs in a single cacheline.
2. The cache array can be mix of empty slots or memcg charged slots, so
the kernel has to traverse the full array.
3. The cache drain from the reclaim will drain all cached memcgs to keep
things simple.

To evaluate the impact of this optimization, on a 72 CPUs machine, we ran
the following workload where each netperf client runs in a different
cgroup. The next-20250415 kernel is used as base.

$ netserver -6
$ netperf -6 -H ::1 -l 60 -t TCP_SENDFILE -- -m 10K

number of clients | Without patch | With patch
6 | 42584.1 Mbps | 48603.4 Mbps (14.13% improvement)
12 | 30617.1 Mbps | 47919.7 Mbps (56.51% improvement)
18 | 25305.2 Mbps | 45497.3 Mbps (79.79% improvement)
24 | 20104.1 Mbps | 37907.7 Mbps (88.55% improvement)
30 | 14702.4 Mbps | 30746.5 Mbps (109.12% improvement)
36 | 10801.5 Mbps | 26476.3 Mbps (145.11% improvement)

The results show drastic improvement for network intensive workloads.

[shakeel.butt@linux.dev: add BUILD_BUG_ON() for MEMCG_CHARGE_BATCH]
Link: https://lkml.kernel.org/r/rlsgeosg3j7v5nihhbxxxbv3xfy4ejvigihj7lkkbt3n6imyne@2apxx2jm2e57
[shakeel.butt@linux.dev: simplify refill_stock]
Link: https://lkml.kernel.org/r/as5cdsm4lraxupg3t6onep2ixql72za25hvd4x334dsoyo4apr@zyzl4vkuevuv
[hughd@google.com: it's better to stock nr_pages than the uninitialized stock_pages]
Link: https://lkml.kernel.org/r/d542d18f-1caa-6fea-e2c3-3555c87bcf64@google.com
[shakeel.butt@linux.dev: add comment per Michal and use DEFINE_PER_CPU_ALIGNED instead of DEFINE_PER_CPU per Vlastimil]
Link: https://lkml.kernel.org/r/dieeei3squ2gcnqxdjayvxbvzldr266rhnvtl3vjzsqevxkevf@ckui5vjzl2qg
Link: https://lkml.kernel.org/r/20250416180229.2902751-1-shakeel.butt@linux.dev
Signed-off-by: Shakeel Butt <shakeel.butt@linux.dev>
Signed-off-by: Hugh Dickins <hughd@google.com>
Acked-by: Jakub Kicinski <kuba@kernel.org>
Acked-by: Michal Hocko <mhocko@suse.com>
Cc: Eric Dumaze <edumazet@google.com>
Cc: Johannes Weiner <hannes@cmpxchg.org>
Cc: Muchun Song <muchun.song@linux.dev>
Cc: Roman Gushchin <roman.gushchin@linux.dev>
Cc: Soheil Hassas Yeganeh <soheil@google.com>
Cc: Vlastimil Babka <vbabka@suse.cz>
Cc: Michal Hocko <mhocko@kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>

authored by

Shakeel Butt and committed by
Andrew Morton
f735eebe 06340b92

+106 -38
+106 -38
mm/memcontrol.c
··· 1769 1769 pr_cont(" are going to be killed due to memory.oom.group set\n"); 1770 1770 } 1771 1771 1772 + /* 1773 + * The value of NR_MEMCG_STOCK is selected to keep the cached memcgs and their 1774 + * nr_pages in a single cacheline. This may change in future. 1775 + */ 1776 + #define NR_MEMCG_STOCK 7 1772 1777 struct memcg_stock_pcp { 1773 1778 local_trylock_t stock_lock; 1774 - struct mem_cgroup *cached; /* this never be root cgroup */ 1775 - unsigned int nr_pages; 1779 + uint8_t nr_pages[NR_MEMCG_STOCK]; 1780 + struct mem_cgroup *cached[NR_MEMCG_STOCK]; 1776 1781 1777 1782 struct obj_cgroup *cached_objcg; 1778 1783 struct pglist_data *cached_pgdat; ··· 1789 1784 unsigned long flags; 1790 1785 #define FLUSHING_CACHED_CHARGE 0 1791 1786 }; 1792 - static DEFINE_PER_CPU(struct memcg_stock_pcp, memcg_stock) = { 1787 + static DEFINE_PER_CPU_ALIGNED(struct memcg_stock_pcp, memcg_stock) = { 1793 1788 .stock_lock = INIT_LOCAL_TRYLOCK(stock_lock), 1794 1789 }; 1795 1790 static DEFINE_MUTEX(percpu_charge_mutex); ··· 1814 1809 gfp_t gfp_mask) 1815 1810 { 1816 1811 struct memcg_stock_pcp *stock; 1817 - unsigned int stock_pages; 1812 + uint8_t stock_pages; 1818 1813 unsigned long flags; 1819 1814 bool ret = false; 1815 + int i; 1820 1816 1821 1817 if (nr_pages > MEMCG_CHARGE_BATCH) 1822 1818 return ret; ··· 1828 1822 return ret; 1829 1823 1830 1824 stock = this_cpu_ptr(&memcg_stock); 1831 - stock_pages = READ_ONCE(stock->nr_pages); 1832 - if (memcg == READ_ONCE(stock->cached) && stock_pages >= nr_pages) { 1833 - WRITE_ONCE(stock->nr_pages, stock_pages - nr_pages); 1834 - ret = true; 1825 + 1826 + for (i = 0; i < NR_MEMCG_STOCK; ++i) { 1827 + if (memcg != READ_ONCE(stock->cached[i])) 1828 + continue; 1829 + 1830 + stock_pages = READ_ONCE(stock->nr_pages[i]); 1831 + if (stock_pages >= nr_pages) { 1832 + WRITE_ONCE(stock->nr_pages[i], stock_pages - nr_pages); 1833 + ret = true; 1834 + } 1835 + break; 1835 1836 } 1836 1837 1837 1838 local_unlock_irqrestore(&memcg_stock.stock_lock, flags); ··· 1856 1843 /* 1857 1844 * Returns stocks cached in percpu and reset cached information. 1858 1845 */ 1859 - static void drain_stock(struct memcg_stock_pcp *stock) 1846 + static void drain_stock(struct memcg_stock_pcp *stock, int i) 1860 1847 { 1861 - unsigned int stock_pages = READ_ONCE(stock->nr_pages); 1862 - struct mem_cgroup *old = READ_ONCE(stock->cached); 1848 + struct mem_cgroup *old = READ_ONCE(stock->cached[i]); 1849 + uint8_t stock_pages; 1863 1850 1864 1851 if (!old) 1865 1852 return; 1866 1853 1854 + stock_pages = READ_ONCE(stock->nr_pages[i]); 1867 1855 if (stock_pages) { 1868 1856 memcg_uncharge(old, stock_pages); 1869 - WRITE_ONCE(stock->nr_pages, 0); 1857 + WRITE_ONCE(stock->nr_pages[i], 0); 1870 1858 } 1871 1859 1872 1860 css_put(&old->css); 1873 - WRITE_ONCE(stock->cached, NULL); 1861 + WRITE_ONCE(stock->cached[i], NULL); 1862 + } 1863 + 1864 + static void drain_stock_fully(struct memcg_stock_pcp *stock) 1865 + { 1866 + int i; 1867 + 1868 + for (i = 0; i < NR_MEMCG_STOCK; ++i) 1869 + drain_stock(stock, i); 1874 1870 } 1875 1871 1876 1872 static void drain_local_stock(struct work_struct *dummy) ··· 1896 1874 1897 1875 stock = this_cpu_ptr(&memcg_stock); 1898 1876 drain_obj_stock(stock); 1899 - drain_stock(stock); 1877 + drain_stock_fully(stock); 1900 1878 clear_bit(FLUSHING_CACHED_CHARGE, &stock->flags); 1901 1879 1902 1880 local_unlock_irqrestore(&memcg_stock.stock_lock, flags); ··· 1905 1883 static void refill_stock(struct mem_cgroup *memcg, unsigned int nr_pages) 1906 1884 { 1907 1885 struct memcg_stock_pcp *stock; 1908 - unsigned int stock_pages; 1886 + struct mem_cgroup *cached; 1887 + uint8_t stock_pages; 1909 1888 unsigned long flags; 1889 + bool success = false; 1890 + int empty_slot = -1; 1891 + int i; 1892 + 1893 + /* 1894 + * For now limit MEMCG_CHARGE_BATCH to 127 and less. In future if we 1895 + * decide to increase it more than 127 then we will need more careful 1896 + * handling of nr_pages[] in struct memcg_stock_pcp. 1897 + */ 1898 + BUILD_BUG_ON(MEMCG_CHARGE_BATCH > S8_MAX); 1910 1899 1911 1900 VM_WARN_ON_ONCE(mem_cgroup_is_root(memcg)); 1912 1901 1913 - if (!local_trylock_irqsave(&memcg_stock.stock_lock, flags)) { 1902 + if (nr_pages > MEMCG_CHARGE_BATCH || 1903 + !local_trylock_irqsave(&memcg_stock.stock_lock, flags)) { 1914 1904 /* 1915 - * In case of unlikely failure to lock percpu stock_lock 1916 - * uncharge memcg directly. 1905 + * In case of larger than batch refill or unlikely failure to 1906 + * lock the percpu stock_lock, uncharge memcg directly. 1917 1907 */ 1918 1908 memcg_uncharge(memcg, nr_pages); 1919 1909 return; 1920 1910 } 1921 1911 1922 1912 stock = this_cpu_ptr(&memcg_stock); 1923 - if (READ_ONCE(stock->cached) != memcg) { /* reset if necessary */ 1924 - drain_stock(stock); 1925 - css_get(&memcg->css); 1926 - WRITE_ONCE(stock->cached, memcg); 1913 + for (i = 0; i < NR_MEMCG_STOCK; ++i) { 1914 + cached = READ_ONCE(stock->cached[i]); 1915 + if (!cached && empty_slot == -1) 1916 + empty_slot = i; 1917 + if (memcg == READ_ONCE(stock->cached[i])) { 1918 + stock_pages = READ_ONCE(stock->nr_pages[i]) + nr_pages; 1919 + WRITE_ONCE(stock->nr_pages[i], stock_pages); 1920 + if (stock_pages > MEMCG_CHARGE_BATCH) 1921 + drain_stock(stock, i); 1922 + success = true; 1923 + break; 1924 + } 1927 1925 } 1928 - stock_pages = READ_ONCE(stock->nr_pages) + nr_pages; 1929 - WRITE_ONCE(stock->nr_pages, stock_pages); 1930 1926 1931 - if (stock_pages > MEMCG_CHARGE_BATCH) 1932 - drain_stock(stock); 1927 + if (!success) { 1928 + i = empty_slot; 1929 + if (i == -1) { 1930 + i = get_random_u32_below(NR_MEMCG_STOCK); 1931 + drain_stock(stock, i); 1932 + } 1933 + css_get(&memcg->css); 1934 + WRITE_ONCE(stock->cached[i], memcg); 1935 + WRITE_ONCE(stock->nr_pages[i], nr_pages); 1936 + } 1933 1937 1934 1938 local_unlock_irqrestore(&memcg_stock.stock_lock, flags); 1939 + } 1940 + 1941 + static bool is_drain_needed(struct memcg_stock_pcp *stock, 1942 + struct mem_cgroup *root_memcg) 1943 + { 1944 + struct mem_cgroup *memcg; 1945 + bool flush = false; 1946 + int i; 1947 + 1948 + rcu_read_lock(); 1949 + 1950 + if (obj_stock_flush_required(stock, root_memcg)) { 1951 + flush = true; 1952 + goto out; 1953 + } 1954 + 1955 + for (i = 0; i < NR_MEMCG_STOCK; ++i) { 1956 + memcg = READ_ONCE(stock->cached[i]); 1957 + if (!memcg) 1958 + continue; 1959 + 1960 + if (READ_ONCE(stock->nr_pages[i]) && 1961 + mem_cgroup_is_descendant(memcg, root_memcg)) { 1962 + flush = true; 1963 + break; 1964 + } 1965 + } 1966 + out: 1967 + rcu_read_unlock(); 1968 + return flush; 1935 1969 } 1936 1970 1937 1971 /* ··· 2011 1933 curcpu = smp_processor_id(); 2012 1934 for_each_online_cpu(cpu) { 2013 1935 struct memcg_stock_pcp *stock = &per_cpu(memcg_stock, cpu); 2014 - struct mem_cgroup *memcg; 2015 - bool flush = false; 2016 - 2017 - rcu_read_lock(); 2018 - memcg = READ_ONCE(stock->cached); 2019 - if (memcg && READ_ONCE(stock->nr_pages) && 2020 - mem_cgroup_is_descendant(memcg, root_memcg)) 2021 - flush = true; 2022 - else if (obj_stock_flush_required(stock, root_memcg)) 2023 - flush = true; 2024 - rcu_read_unlock(); 1936 + bool flush = is_drain_needed(stock, root_memcg); 2025 1937 2026 1938 if (flush && 2027 1939 !test_and_set_bit(FLUSHING_CACHED_CHARGE, &stock->flags)) { ··· 2037 1969 drain_obj_stock(stock); 2038 1970 local_unlock_irqrestore(&memcg_stock.stock_lock, flags); 2039 1971 2040 - drain_stock(stock); 1972 + drain_stock_fully(stock); 2041 1973 2042 1974 return 0; 2043 1975 }