Introduce QPW for per-cpu operations
Subsystems (8)
cover Introduce QPW for per-cpu operations
The problem:
Some places in the kernel implement a parallel programming strategy consisting on local_locks() for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.
On the other hand, for RT workloads this can represent a problem: getting an important workload scheduled out to deal with remote requests is sure to introduce unexpected deadline misses.
The idea:
Currently with PREEMPT_RT=y, local_locks() become per-cpu spinlocks. In this case, instead of scheduling work on a remote cpu, it should be safe to grab that remote cpu's per-cpu spinlock and run the required work locally. That major cost, which is un/locking in every local function, already happens in PREEMPT_RT.
Also, there is no need to worry about extra cache bouncing:
The cacheline invalidation already happens due to schedule_work_on().
This will avoid schedule_work_on(), and thus avoid scheduling-out an RT workload.
Proposed solution:
A new interface called Queue PerCPU Work (QPW), which should replace Work Queue in the above mentioned use case.
If PREEMPT_RT=n this interfaces just wraps the current local_locks + WorkQueue behavior, so no expected change in runtime.
If PREEMPT_RT=y, or CONFIG_QPW=y, queue_percpu_work_on(cpu,...) will lock that cpu's per-cpu structure and perform work on it locally. This is possible because on functions that can be used for performing remote work on remote per-cpu structures, the local_lock (which is already a this_cpu spinlock()), will be replaced by a qpw_spinlock(), which is able to get the per_cpu spinlock() for the cpu passed as parameter.
RFC->v1:
- Introduce CONFIG_QPW and qpw= kernel boot option to enable remote spinlocking and execution even on !CONFIG_PREEMPT_RT kernels (Leonardo Bras).
- Move buffer_head draining to separate workqueue (Marcelo Tosatti).
- Convert mlock per-CPU page lists to QPW (Marcelo Tosatti).
- Drop memcontrol convertion (as isolated CPUs are not targets of queue_work_on anymore).
- Rebase SLUB against Vlastimil's slab/next.
- Add basic document for QPW (Waiman Long).
The following testcase triggers lru_add_drain_all on an isolated CPU (that does sys_write to a file before entering its realtime loop).
/*
* Simulates a low latency loop program that is interrupted
* due to lru_add_drain_all. To trigger lru_add_drain_all, run:
*
* blockdev --flushbufs /dev/sdX
*
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdarg.h>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
int cpu;
static void *run(void *arg)
{
pthread_t current_thread;
cpu_set_t cpuset;
int ret, nrloops;
struct sched_param sched_p;
pid_t pid;
int fd;
char buf[] = "xxxxxxxxxxx";
CPU_ZERO(&cpuset);
CPU_SET(cpu, &cpuset);
current_thread = pthread_self();
ret = pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset);
if (ret) {
perror("pthread_setaffinity_np failed\n");
exit(0);
}
memset(&sched_p, 0, sizeof(struct sched_param));
sched_p.sched_priority = 1;
pid = gettid();
ret = sched_setscheduler(pid, SCHED_FIFO, &sched_p);
if (ret) {
perror("sched_setscheduler");
exit(0);
}
fd = open("/tmp/tmpfile", O_RDWR|O_CREAT|O_TRUNC);
if (fd == -1) {
perror("open");
exit(0);
}
ret = write(fd, buf, sizeof(buf));
if (ret == -1) {
perror("write");
exit(0);
}
do {
nrloops = nrloops+2;
nrloops--;
} while (1);
}int main(int argc, char *argv[])
{
int fd, ret;
pthread_t thread;
long val;
char *endptr, *str;
struct sched_param sched_p;
pid_t pid;
if (argc != 2) {
printf("usage: %s cpu-nr\n", argv[0]);
printf("where CPU number is the CPU to pin thread to\n");
exit(0);
}
str = argv[1];
cpu = strtol(str, &endptr, 10);
if (cpu < 0) {
printf("strtol returns %d\n", cpu);
exit(0);
}
printf("cpunr=%d\n", cpu);
memset(&sched_p, 0, sizeof(struct sched_param));
sched_p.sched_priority = 1;
pid = getpid();
ret = sched_setscheduler(pid, SCHED_FIFO, &sched_p);
if (ret) {
perror("sched_setscheduler");
exit(0);
}
pthread_create(&thread, NULL, run, NULL);
sleep(5000);
pthread_join(thread, NULL);
}PATCH 1/4 Introducing qpw_lock() and per-cpu queue & flush work
Some places in the kernel implement a parallel programming strategy consisting on local_locks() for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.
On the other hand, for RT workloads this can represent a problem:
scheduling work on remote cpu that are executing low latency tasks is undesired and can introduce unexpected deadline misses.
It's interesting, though, that local_lock()s in RT kernels become spinlock(). We can make use of those to avoid scheduling work on a remote cpu by directly updating another cpu's per_cpu structure, while holding it's spinlock().
In order to do that, it's necessary to introduce a new set of functions to make it possible to get another cpu's per-cpu "local" lock (qpw_{un,}lock*) and also the corresponding queue_percpu_work_on() and flush_percpu_work() helpers to run the remote work.
Users of non-RT kernels but with low latency requirements can select similar functionality by using the CONFIG_QPW compile time option.
On CONFIG_QPW disabled kernels, no changes are expected, as every one of the introduced helpers work the exactly same as the current implementation:
qpw_{un,}lock*() -> local_{un,}lock*() (ignores cpu parameter)
queue_percpu_work_on() -> queue_work_on()
flush_percpu_work() -> flush_work()For QPW enabled kernels, though, qpw_{un,}lock*() will use the extra cpu parameter to select the correct per-cpu structure to work on, and acquire the spinlock for that cpu.
queue_percpu_work_on() will just call the requested function in the current cpu, which will operate in another cpu's per-cpu object. Since the local_locks() become spinlock()s in QPW enabled kernels, we are safe doing that.
flush_percpu_work() then becomes a no-op since no work is actually scheduled on a remote cpu.
Some minimal code rework is needed in order to make this mechanism work:
The calls for local_{un,}lock*() on the functions that are currently scheduled on remote cpus need to be replaced by qpw_{un,}lock_n*(), so in QPW enabled kernels they can reference a different cpu. It's also necessary to use a qpw_struct instead of a work_struct, but it just contains a work struct and, in CONFIG_QPW, the target cpu.
This should have almost no impact on non-CONFIG_QPW kernels: few this_cpu_ptr() will become per_cpu_ptr(,smp_processor_id()).
On CONFIG_QPW kernels, this should avoid deadlines misses by removing scheduling noise.
--- Documentation/admin-guide/kernel-parameters.txt | 10 + Documentation/locking/qpwlocks.rst | 63 +++++++ MAINTAINERS | 6 include/linux/qpw.h | 190 ++++++++++++++++++++++++ init/Kconfig | 35 ++++ kernel/Makefile | 2 kernel/qpw.c | 26 +++ 7 files changed, 332 insertions(+) create mode 100644 include/linux/qpw.h create mode 100644 kernel/qpw.c
Documentation/admin-guide/kernel-parameters.txt +10 -0
@@ -2819,6 +2819,16 @@ Kernel parameters The format of <cpu-list> is described above. + qpw= [KNL,SMP] Select a behavior on per-CPU resource sharing + and remote interference mechanism on a kernel built with + CONFIG_QPW. + Format: { "0" | "1" } + 0 - local_lock() + queue_work_on(remote_cpu) + 1 - spin_lock() for both local and remote operations + + Selecting 1 may be interesting for systems that want + to avoid interruption & context switches from IPIs. + iucv= [HW,NET] ivrs_ioapic [HW,X86-64]
MAINTAINERS +6 -0
@@ -21291,6 +21291,12 @@ F: Documentation/networking/device_drive F: drivers/bus/fsl-mc/ F: include/uapi/linux/fsl_mc.h +QPW +M: Leonardo Bras <[email protected]> +S: Supported +F: include/linux/qpw.h +F: kernel/qpw.c + QT1010 MEDIA DRIVER L: [email protected] S: Orphan
include/linux/qpw.h new file +190 -0
190 lines not on this page · raw message
init/Kconfig +35 -0
@@ -747,6 +747,41 @@ config CPU_ISOLATION Say Y if unsure. +config QPW + bool "Queue per-CPU Work" + depends on SMP || COMPILE_TEST + default n + help + Allow changing the behavior on per-CPU resource sharing with cache, + from the regular local_locks() + queue_work_on(remote_cpu) to using + per-CPU spinlocks on both local and remote operations. + + This is useful to give user the option on reducing IPIs to CPUs, and + thus reduce interruptions and context switches. On the other hand, it + increases generated code and will use atomic operations if spinlocks + are selected. + + If set, will use the default behavior set in QPW_DEFAULT unless boot + parameter qpw is passed with a different behavior. + + If unset, will use the local_lock() + queue_work_on() strategy, + regardless of the boot parameter or QPW_DEFAULT. + + Say N if unsure. + +config QPW_DEFAULT + bool "Use per-CPU spinlocks by default" + depends on QPW + default n + help + If set, will use per-CPU spinlocks as default behavior for per-CPU + remote operations. + + If unset, will use local_lock() + queue_work_on(cpu) as default + behavior for remote operations. + + Say N if unsure + source "kernel/rcu/Kconfig" config IKCONFIG
kernel/Makefile +2 -0
@@ -140,6 +140,8 @@ obj-$(CONFIG_WATCH_QUEUE) += watch_queue obj-$(CONFIG_RESOURCE_KUNIT_TEST) += resource_kunit.o obj-$(CONFIG_SYSCTL_KUNIT_TEST) += sysctl-test.o +obj-$(CONFIG_QPW) += qpw.o + CFLAGS_kstack_erase.o += $(DISABLE_KSTACK_ERASE) CFLAGS_kstack_erase.o += $(call cc-option,-mgeneral-regs-only) obj-$(CONFIG_KSTACK_ERASE) += kstack_erase.o
kernel/qpw.c new file +26 -0
@@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "linux/export.h" +#include <linux/sched.h> +#include <linux/qpw.h> +#include <linux/string.h> + +DEFINE_STATIC_KEY_MAYBE (CONFIG_QPW_DEFAULT , qpw_sl); +EXPORT_SYMBOL (qpw_sl); + +static int __init qpw_setup (char *str) +{ +int opt; + +if (!get_option (&str, &opt)) { +pr_warn ("QPW: invalid qpw parameter: %s, ignoring.\n" , str); +return 0 ; + } + +if (opt) +static_branch_enable (&qpw_sl); +else +static_branch_disable (&qpw_sl); + +return 0 ; +} +__setup ("qpw=" , qpw_setup);
Documentation/locking/qpwlocks.rst new file +63 -0
63 lines not on this page · raw message
Re: [PATCH 1/4] Introducing qpw_lock() and per-cpu queue & flush work
Quoted 6 lines · Marcelo Tosatti
Some places in the kernel implement a parallel programming strategy consisting on
local_locks()for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.
Forgot to mention: patchset is against Vlastimil's slab/next tree.
Re: [PATCH 1/4] Introducing qpw_lock() and per-cpu queue & flush work
In reply to Marcelo TosattiQuoted 96 lines · Marcelo Tosatti
Some places in the kernel implement a parallel programming strategy consisting on
local_locks()for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.On the other hand, for RT workloads this can represent a problem:
scheduling work on remote cpu that are executing low latency tasks is undesired and can introduce unexpected deadline misses.It's interesting, though, that local_lock()s in RT kernels become
spinlock(). We can make use of those to avoid scheduling work on a remote cpu by directly updating another cpu'sper_cpustructure, while holding it'sspinlock().In order to do that, it's necessary to introduce a new set of functions to make it possible to get another cpu's per-cpu "local" lock (qpw_{un,}lock*) and also the corresponding
queue_percpu_work_on()andflush_percpu_work()helpers to run the remote work.Users of non-RT kernels but with low latency requirements can select similar functionality by using the
CONFIG_QPWcompile time option.On
CONFIG_QPWdisabled kernels, no changes are expected, as every one of the introduced helpers work the exactly same as the current implementation:qpw_{un,}lock*() -> local_{un,}lock*() (ignores cpu parameter) queue_percpu_work_on() -> queue_work_on() flush_percpu_work() -> flush_work()For QPW enabled kernels, though, qpw_{un,}lock*() will use the extra cpu parameter to select the correct per-cpu structure to work on, and acquire the spinlock for that cpu.
queue_percpu_work_on()will just call the requested function in the current cpu, which will operate in another cpu's per-cpu object. Since thelocal_locks()become spinlock()s in QPW enabled kernels, we are safe doing that.
flush_percpu_work()then becomes a no-op since no work is actually scheduled on a remote cpu.Some minimal code rework is needed in order to make this mechanism work:
The calls for local_{un,}lock*() on the functions that are currently scheduled on remote cpus need to be replaced by qpw_{un,}lock_n*(), so in QPW enabled kernels they can reference a different cpu. It's also necessary to use aqpw_structinstead of awork_struct, but it just contains a work struct and, inCONFIG_QPW, the target cpu.This should have almost no impact on non-CONFIG_QPW kernels: few
this_cpu_ptr()will become per_cpu_ptr(,smp_processor_id()).On
CONFIG_QPWkernels, this should avoid deadlines misses by removing scheduling noise.Signed-off-by Leonardo Bras <[email protected]>Signed-off-by Marcelo Tosatti <[email protected]>--- Documentation/admin-guide/kernel-parameters.txt | 10 + Documentation/locking/qpwlocks.rst | 63 +++++++ MAINTAINERS | 6 include/linux/qpw.h | 190 ++++++++++++++++++++++++ init/Kconfig | 35 ++++ kernel/Makefile | 2 kernel/qpw.c | 26 +++ 7 files changed, 332 insertions(+) create mode 100644 include/linux/qpw.h create mode 100644 kernel/qpw.cIndex:
slab/Documentation/admin-guide/kernel-parameters.txt
===================================================================Documentation/admin-guide/kernel-parameters.txt +10
-0@@ -2819,6 +2819,16 @@ Kernel parameters The format of <cpu-list> is described above. + qpw= [KNL,SMP] Select a behavior on per-CPU resource sharing + and remote interference mechanism on a kernel built with + CONFIG_QPW. + Format: { "0" | "1" } + 0 - local_lock() + queue_work_on(remote_cpu) + 1 - spin_lock() for both local and remote operations + + Selecting 1 may be interesting for systems that want + to avoid interruption & context switches from IPIs. + iucv= [HW,NET] ivrs_ioapic [HW,X86-64]Index: slab/MAINTAINERS
===================================================================
MAINTAINERS F: Documentation/networking/device_drive · line 21295
Quoted 5 lines
MAINTAINERS +2
-0@@ -21291,6 +21291,12 @@ F: Documentation/networking/device_drive F: drivers/bus/fsl-mc/ F: include/uapi/linux/fsl_mc.h +QPW +M: Leonardo Bras <[email protected]>
Thanks for keeping that up :)
Could you please change this line to
+M: Leonardo Bras <[email protected]>
As I don't have access to Red Hat's mail anymore. The signoffs on each commit should be fine to keep :)
MAINTAINERS F: Documentation/networking/device_drive · line 21299
Quoted 4 lines
MAINTAINERS +4
-0+S: Supported +F: include/linux/qpw.h +F: kernel/qpw.c +
Should we also add the Documentation file as well?
+F: Documentation/locking/qpwlocks.rst
Quoted 325 lines
MAINTAINERS +0
-0QT1010 MEDIA DRIVER L: [email protected] S: OrphanIndex:
slab/include/linux/qpw.h
===================================================================include/linux/qpw.h new file +190
-0@@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_QPW_H +#define _LINUX_QPW_H + +#include "linux/spinlock.h" +#include "linux/local_lock.h" +#include "linux/workqueue.h" + +#ifndef CONFIG_QPW + +typedef local_lock_t qpw_lock_t ; +typedef local_trylock_t qpw_trylock_t ; + +struct qpw_struct { +struct work_struct work; +}; + +#define qpw_lock_init (lock) \ + local_lock_init(lock) + +#define qpw_trylock_init (lock) \ + local_trylock_init(lock) + +#define qpw_lock (lock, cpu) \ + local_lock(lock) + +#define qpw_lock_irqsave (lock, flags, cpu) \ + local_lock_irqsave(lock, flags) + +#define qpw_trylock (lock, cpu) \ + local_trylock(lock) + +#define qpw_trylock_irqsave (lock, flags, cpu) \ + local_trylock_irqsave(lock, flags) + +#define qpw_unlock (lock, cpu) \ + local_unlock(lock) + +#define qpw_unlock_irqrestore (lock, flags, cpu) \ + local_unlock_irqrestore(lock, flags) + +#define qpw_lockdep_assert_held (lock) \ + lockdep_assert_held(lock) + +#define queue_percpu_work_on (c, wq, qpw) \ + queue_work_on(c, wq, &(qpw)->work) + +#define flush_percpu_work (qpw) \ + flush_work(&(qpw)->work) + +#define qpw_get_cpu (qpw) smp_processor_id() + +#define qpw_is_cpu_remote (cpu) (false) + +#define INIT_QPW (qpw, func, c) \ + INIT_WORK(&(qpw)->work, (func)) + +#else /* CONFIG_QPW */ + +DECLARE_STATIC_KEY_MAYBE (CONFIG_QPW_DEFAULT , qpw_sl); + +typedef union { +spinlock_t sl; +local_lock_t ll; +}qpw_lock_t ; + +typedef union { +spinlock_t sl; +local_trylock_t ll; +}qpw_trylock_t ; + +struct qpw_struct { +struct work_struct work; +int cpu; +}; + +#define qpw_lock_init (lock) \ + do { \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \ + spin_lock_init(lock.sl); \ + else \ + local_lock_init(lock.ll); \ + } while (0) + +#define qpw_trylock_init (lock) \ + do { \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \ + spin_lock_init(lock.sl); \ + else \ + local_trylock_init(lock.ll); \ + } while (0) + +#define qpw_lock (lock, cpu) \ + do { \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \ + spin_lock(per_cpu_ptr(lock.sl, cpu)); \ + else \ + local_lock(lock.ll); \ + } while (0) + +#define qpw_lock_irqsave (lock, flags, cpu) \ + do { \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \ + spin_lock_irqsave(per_cpu_ptr(lock.sl, cpu), flags); \ + else \ + local_lock_irqsave(lock.ll, flags); \ + } while (0) + +#define qpw_trylock (lock, cpu) \ + ({ \ + int t; \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \ + t = spin_trylock(per_cpu_ptr(lock.sl, cpu)); \ + else \ + t = local_trylock(lock.ll); \ + t; \ + }) + +#define qpw_trylock_irqsave (lock, flags, cpu) \ + ({ \ + int t; \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \ + t = spin_trylock_irqsave(per_cpu_ptr(lock.sl, cpu), flags); \ + else \ + t = local_trylock_irqsave(lock.ll, flags); \ + t; \ + }) + +#define qpw_unlock (lock, cpu) \ + do { \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) { \ + spin_unlock(per_cpu_ptr(lock.sl, cpu)); \ + } else { \ + local_unlock(lock.ll); \ + } \ + } while (0) + +#define qpw_unlock_irqrestore (lock, flags, cpu) \ + do { \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \ + spin_unlock_irqrestore(per_cpu_ptr(lock.sl, cpu), flags); \ + else \ + local_unlock_irqrestore(lock.ll, flags); \ + } while (0) + +#define qpw_lockdep_assert_held (lock) \ + do { \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \ + lockdep_assert_held(this_cpu_ptr(lock.sl)); \ + else \ + lockdep_assert_held(this_cpu_ptr(lock.ll)); \ + } while (0) + +#define queue_percpu_work_on (c, wq, qpw) \ + do { \ + int __c = c; \ + struct qpw_struct *__qpw = (qpw); \ + if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) { \ + WARN_ON((__c) != __qpw->cpu); \ + __qpw->work.func(&__qpw->work); \ + } else { \ + queue_work_on(__c, wq, &(__qpw)->work); \ + } \ + } while (0) + +/* +* Does nothing if QPW is set to use spinlock, as the task is already done at the +* time queue_percpu_work_on() returns. +*/ +#define flush_percpu_work (qpw) \ + do { \ + struct qpw_struct *__qpw = (qpw); \ + if (!static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) { \ + flush_work(&__qpw->work); \ + } \ + } while (0) + +#define qpw_get_cpu (w) container_of((w), struct qpw_struct, work)->cpu + +#define qpw_is_cpu_remote (cpu) ((cpu) != smp_processor_id()) + +#define INIT_QPW (qpw, func, c) \ + do { \ + struct qpw_struct *__qpw = (qpw); \ + INIT_WORK(&__qpw->work, (func)); \ + __qpw->cpu = (c); \ + } while (0) + +#endif /* CONFIG_QPW */ +#endif /* LINUX_QPW_H */ Index: slab/init/Kconfig
===================================================================init/Kconfig +35
-0@@ -747,6 +747,41 @@ config CPU_ISOLATION Say Y if unsure. +config QPW + bool "Queue per-CPU Work" + depends on SMP || COMPILE_TEST + default n + help + Allow changing the behavior on per-CPU resource sharing with cache, + from the regular local_locks() + queue_work_on(remote_cpu) to using + per-CPU spinlocks on both local and remote operations. + + This is useful to give user the option on reducing IPIs to CPUs, and + thus reduce interruptions and context switches. On the other hand, it + increases generated code and will use atomic operations if spinlocks + are selected. + + If set, will use the default behavior set in QPW_DEFAULT unless boot + parameter qpw is passed with a different behavior. + + If unset, will use the local_lock() + queue_work_on() strategy, + regardless of the boot parameter or QPW_DEFAULT. + + Say N if unsure. + +config QPW_DEFAULT + bool "Use per-CPU spinlocks by default" + depends on QPW + default n + help + If set, will use per-CPU spinlocks as default behavior for per-CPU + remote operations. + + If unset, will use local_lock() + queue_work_on(cpu) as default + behavior for remote operations. + + Say N if unsure + source "kernel/rcu/Kconfig" config IKCONFIGIndex: slab/kernel/Makefile
===================================================================kernel/Makefile +2
-0@@ -140,6 +140,8 @@ obj-$(CONFIG_WATCH_QUEUE) += watch_queue obj-$(CONFIG_RESOURCE_KUNIT_TEST) += resource_kunit.o obj-$(CONFIG_SYSCTL_KUNIT_TEST) += sysctl-test.o +obj-$(CONFIG_QPW) += qpw.o + CFLAGS_kstack_erase.o += $(DISABLE_KSTACK_ERASE) CFLAGS_kstack_erase.o += $(call cc-option,-mgeneral-regs-only) obj-$(CONFIG_KSTACK_ERASE) += kstack_erase.oIndex:
slab/kernel/qpw.c
===================================================================kernel/qpw.c new file +26
-0@@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "linux/export.h" +#include <linux/sched.h> +#include <linux/qpw.h> +#include <linux/string.h> + +DEFINE_STATIC_KEY_MAYBE (CONFIG_QPW_DEFAULT , qpw_sl); +EXPORT_SYMBOL (qpw_sl); + +static int __init qpw_setup (char *str) +{ +int opt; + +if (!get_option (&str, &opt)) { +pr_warn ("QPW: invalid qpw parameter: %s, ignoring.\n" , str); +return 0 ; + } + +if (opt) +static_branch_enable (&qpw_sl); +else +static_branch_disable (&qpw_sl); + +return 0 ; +} +__setup ("qpw=" , qpw_setup);Index:
slab/Documentation/locking/qpwlocks.rst
===================================================================Documentation/locking/qpwlocks.rst new file +32
-0@@ -0,0 +1,63 @@ +.. SPDX-License-Identifier: GPL-2.0 + +========= +QPW locks +========= + +Some places in the kernel implement a parallel programming strategy +consisting on local_locks() for most of the work, and some rare remote +operations are scheduled on target cpu. This keeps cache bouncing low since +cacheline tends to be mostly local, and avoids the cost of locks in non-RT +kernels, even though the very few remote operations will be expensive due +to scheduling overhead. + +On the other hand, for RT workloads this can represent a problem: +scheduling work on remote cpu that are executing low latency tasks +is undesired and can introduce unexpected deadline misses. + +QPW locks help to convert sites that use local_locks (for cpu local operations) +and queue_work_on (for queueing work remotely, to be executed +locally on the owner cpu of the lock) to QPW locks. + +The lock is declared qpw_lock_t type. +The lock is initialized with qpw_lock_init. +The lock is locked with qpw_lock (takes a lock and cpu as a parameter). +The lock is unlocked with qpw_unlock (takes a lock and cpu as a parameter). + +The qpw_lock_irqsave function disables interrupts and saves current interrupt state, +cpu as a parameter. + +For trylock variant, there is the qpw_trylock_t type, initialized with +qpw_trylock_init. Then the corresponding qpw_trylock and +qpw_trylock_irqsave.
Documentation/locking/qpwlocks.rst line 42
Quoted 10 lines
Documentation/locking/qpwlocks.rst new file +10
-0+ +work_struct should be replaced by qpw_struct, which contains a cpu parameter +(owner cpu of the lock), initialized by INIT_QPW. + +The queue work related functions (analogous to queue_work_on and flush_work) are: +queue_percpu_work_on and flush_percpu_work. + +The behaviour of the QPW functions is as follows: + +* !CONFIG_PREEMPT_RT and !CONFIG_QPW (or CONFIG_QPW and qpw=off kernel
I don't think PREEMPT_RT is needed here (maybe it was copied from the previous QPW version which was dependent on PREEMPT_RT?)
Documentation/locking/qpwlocks.rst line 52
Quoted 10 lines
Documentation/locking/qpwlocks.rst new file +10
-0+boot parameter): + - qpw_lock: local_lock + - qpw_lock_irqsave: local_lock_irqsave + - qpw_trylock: local_trylock + - qpw_trylock_irqsave: local_trylock_irqsave + - qpw_unlock: local_unlock + - queue_percpu_work_on: queue_work_on + - flush_percpu_work: flush_work + +* CONFIG_PREEMPT_RT or CONFIG_QPW (and CONFIG_QPW_DEFAULT or qpw=on kernel
Same here
Documentation/locking/qpwlocks.rst line 63
Quoted 13 lines
Documentation/locking/qpwlocks.rst new file +11
-0+boot parameter), + - qpw_lock: spin_lock + - qpw_lock_irqsave: spin_lock_irqsave + - qpw_trylock: spin_trylock + - qpw_trylock_irqsave: spin_trylock_irqsave + - qpw_unlock: spin_unlock + - queue_percpu_work_on: executes work function on caller cpu + - flush_percpu_work: empty + +qpw_get_cpu(work_struct), to be called from within qpw work function, +returns the target cpu.
Other than that, LGTM!
Thanks!
Leo
Re: [PATCH 1/4] Introducing qpw_lock() and per-cpu queue & flush work
Hi Leonardo,
Quoted 13 lines · Leonardo Bras
MAINTAINERS F: Documentation/networking/device_drive · line 21295
Quoted 9 lines
===================================================================
MAINTAINERS +2
-0@@ -21291,6 +21291,12 @@ F: Documentation/networking/device_drive F: drivers/bus/fsl-mc/ F: include/uapi/linux/fsl_mc.h +QPW +M: Leonardo Bras <[email protected]>Thanks for keeping that up :)
Could you please change this line to
Quoted 4 lines
+M: Leonardo Bras <[email protected]>
As I don't have access to Red Hat's mail anymore. The signoffs on each commit should be fine to keep :)
Done.
Quoted 9 lines
MAINTAINERS F: Documentation/networking/device_drive · line 21299
Quoted 5 lines
MAINTAINERS +4
-0+S: Supported +F: include/linux/qpw.h +F: kernel/qpw.c +Should we also add the Documentation file as well?
+F:
Documentation/locking/qpwlocks.rst
Done.
Quoted 9 lines
MAINTAINERS F: Documentation/networking/device_drive
Quoted 6 lines
MAINTAINERS +6
-0+The queue work related functions (analogous to queue_work_on and flush_work) are: +queue_percpu_work_on and flush_percpu_work. + +The behaviour of the QPW functions is as follows: + +* !CONFIG_PREEMPT_RT and !CONFIG_QPW (or CONFIG_QPW and qpw=off kernelI don't think
PREEMPT_RTis needed here (maybe it was copied from the previous QPW version which was dependent onPREEMPT_RT?)
Ah, OK, my bad. Well, shouldnt CONFIG_PREEMPT_RT select CONFIG_QPW and CONFIG_QPW_DEFAULT=y ?
Quoted 36 lines
MAINTAINERS
Quoted 10 lines
MAINTAINERS +10
-0+boot parameter): + - qpw_lock: local_lock + - qpw_lock_irqsave: local_lock_irqsave + - qpw_trylock: local_trylock + - qpw_trylock_irqsave: local_trylock_irqsave + - qpw_unlock: local_unlock + - queue_percpu_work_on: queue_work_on + - flush_percpu_work: flush_work + +* CONFIG_PREEMPT_RT or CONFIG_QPW (and CONFIG_QPW_DEFAULT or qpw=on kernelSame here
MAINTAINERS
Quoted 13 lines
MAINTAINERS +11
-0+boot parameter), + - qpw_lock: spin_lock + - qpw_lock_irqsave: spin_lock_irqsave + - qpw_trylock: spin_trylock + - qpw_trylock_irqsave: spin_trylock_irqsave + - qpw_unlock: spin_unlock + - queue_percpu_work_on: executes work function on caller cpu + - flush_percpu_work: empty + +qpw_get_cpu(work_struct), to be called from within qpw work function, +returns the target cpu.Other than that, LGTM!
Reviewed-by Leonardo Bras <[email protected]>Thanks!
Leo
Re: [PATCH 1/4] Introducing qpw_lock() and per-cpu queue & flush work
Quoted 45 lines · Marcelo Tosatti
Hi Leonardo,
Quoted 13 lines · Leonardo Bras
MAINTAINERS F: Documentation/networking/device_drive · line 21295
Quoted 9 lines
===================================================================
MAINTAINERS +2
-0@@ -21291,6 +21291,12 @@ F: Documentation/networking/device_drive F: drivers/bus/fsl-mc/ F: include/uapi/linux/fsl_mc.h +QPW +M: Leonardo Bras <[email protected]>Thanks for keeping that up :)
Could you please change this line toQuoted 4 lines
+M: Leonardo Bras <[email protected]>
As I don't have access to Red Hat's mail anymore. The signoffs on each commit should be fine to keep :)
Done.
Quoted 9 lines
MAINTAINERS F: Documentation/networking/device_drive · line 21299
Quoted 5 lines
MAINTAINERS +4
-0+S: Supported +F: include/linux/qpw.h +F: kernel/qpw.c +Should we also add the Documentation file as well?
+F:
Documentation/locking/qpwlocks.rstDone.
Quoted 9 lines
MAINTAINERS F: Documentation/networking/device_drive
Quoted 6 lines
MAINTAINERS +6
-0+The queue work related functions (analogous to queue_work_on and flush_work) are: +queue_percpu_work_on and flush_percpu_work. + +The behaviour of the QPW functions is as follows: + +* !CONFIG_PREEMPT_RT and !CONFIG_QPW (or CONFIG_QPW and qpw=off kernelI don't think
PREEMPT_RTis needed here (maybe it was copied from the previous QPW version which was dependent onPREEMPT_RT?)
Quoted 2 lines
Ah, OK, my bad. Well, shouldnt
CONFIG_PREEMPT_RTselectCONFIG_QPWandCONFIG_QPW_DEFAULT=y?
Oh, I sure think it should, even if not doing so at the current patchset.
But my point in above comment is that even if it did, there was no need to mention !RT and !QPW, as RT would select QPW, so you only need to mention QPW :)
Before QPW having it's own CONFIG_ I was using RT to compile this in, so maybe that's why the previous version of the cover letter mentioned it. :\
Thanks!
Leo
PATCH 2/4 mm/swap: move bh draining into a separate workqueue
In reply to Marcelo TosattiSeparate the bh draining into a separate workqueue (from the mm lru draining), so that its possible to switch the mm lru draining to QPW.
To switch bh draining to QPW, it would be necessary to add a spinlock to addition of bhs to percpu cache, and that is a very hot path.
--- mm/swap.c | 52 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 15 deletions(-)
mm/swap.c +37 -15
@@ -745,12 +745,11 @@ void lru_add_drain(void)* the same cpu. It shouldn't be a problem in !SMP case since * the core is only one and the locks will disable preemption. */ -static void lru_add_and_bh_lrus_drain (void ) +static void lru_add_mm_drain (void ) {local_lock (&cpu_fbatches.lock);lru_add_drain_cpu (smp_processor_id ());local_unlock (&cpu_fbatches.lock); -invalidate_bh_lrus_cpu ();mlock_drain_local (); }
@@ -769,10 +768,17 @@ static DEFINE_PER_CPU(struct work_structstatic void lru_add_drain_per_cpu (struct work_struct *dummy) { -lru_add_and_bh_lrus_drain (); +lru_add_mm_drain (); } -static bool cpu_needs_drain (unsigned int cpu) +static DEFINE_PER_CPU(struct work_struct, bh_add_drain_work); + +static void bh_add_drain_per_cpu (struct work_struct *dummy) +{ +invalidate_bh_lrus_cpu (); +} + +static bool cpu_needs_mm_drain (unsigned int cpu) {struct cpu_fbatches *fbatches = &per_cpu (cpu_fbatches, cpu);
@@ -783,8 +789,12 @@ static bool cpu_needs_drain(unsigned intfolio_batch_count (&fbatches->lru_deactivate) ||folio_batch_count (&fbatches->lru_lazyfree) ||folio_batch_count (&fbatches->lru_activate) || -need_mlock_drain (cpu) || -has_bh_in_lru (cpu,NULL ); +need_mlock_drain (cpu); +} + +static bool cpu_needs_bh_drain (unsigned int cpu) +{ +return has_bh_in_lru (cpu,NULL ); }/*
@@ -807,7 +817,7 @@ static inline void __lru_add_drain_all(b* each CPU. */ static unsigned int lru_drain_gen; -static struct cpumask has_work; +static struct cpumask has_mm_work, has_bh_work;static DEFINE_MUTEX (lock);unsigned cpu, this_gen;
@@ -870,20 +880,31 @@ static inline void __lru_add_drain_all(bWRITE_ONCE (lru_drain_gen, lru_drain_gen +1 );smp_mb (); -cpumask_clear (&has_work); +cpumask_clear (&has_mm_work); +cpumask_clear (&has_bh_work);for_each_online_cpu (cpu) { -struct work_struct *work = &per_cpu (lru_add_drain_work, cpu); +struct work_struct *mm_work = &per_cpu (lru_add_drain_work, cpu); +struct work_struct *bh_work = &per_cpu (bh_add_drain_work, cpu); + +if (cpu_needs_mm_drain (cpu)) { +INIT_WORK (mm_work, lru_add_drain_per_cpu); +queue_work_on (cpu, mm_percpu_wq, mm_work); +__cpumask_set_cpu (cpu, &has_mm_work); + } -if (cpu_needs_drain (cpu)) { -INIT_WORK (work, lru_add_drain_per_cpu); -queue_work_on (cpu, mm_percpu_wq, work); -__cpumask_set_cpu (cpu, &has_work); +if (cpu_needs_bh_drain (cpu)) { +INIT_WORK (bh_work, bh_add_drain_per_cpu); +queue_work_on (cpu, mm_percpu_wq, bh_work); +__cpumask_set_cpu (cpu, &has_bh_work); } } -for_each_cpu (cpu, &has_work) +for_each_cpu (cpu, &has_mm_work)flush_work (&per_cpu (lru_add_drain_work, cpu)); +for_each_cpu (cpu, &has_bh_work) +flush_work (&per_cpu (bh_add_drain_work, cpu)); + done:mutex_unlock (&lock); }
@@ -929,7 +950,8 @@ void lru_cache_disable(void)#ifdef CONFIG_SMP __lru_add_drain_all (true);#else -lru_add_and_bh_lrus_drain (); +lru_add_mm_drain (); +invalidate_bh_lrus_cpu ();#endif }
PATCH 3/4 swap: apply new queue_percpu_work_on() interface
In reply to Marcelo TosattiMake use of the new qpw_{un,}lock*() and queue_percpu_work_on() interface to improve performance & latency on PREEMPT_RT kernels.
For functions that may be scheduled in a different cpu, replace local_{un,}lock*() by qpw_{un,}lock*(), and replace schedule_work_on() by queue_percpu_work_on(). The same happens for flush_work() and flush_percpu_work().
The change requires allocation of qpw_structs instead of a work_structs, and changing parameters of a few functions to include the cpu parameter.
This should bring no relevant performance impact on non-RT kernels:
For functions that may be scheduled in a different cpu, the local_*lock's this_cpu_ptr() becomes a per_cpu_ptr(smp_processor_id()).
--- mm/internal.h | 4 +- mm/mlock.c | 71 ++++++++++++++++++++++++++++++++------------ mm/page_alloc.c | 2 - mm/swap.c | 90 +++++++++++++++++++++++++++++++------------------------- 4 files changed, 108 insertions(+), 59 deletions(-)
mm/mlock.c +53 -18
@@ -25,17 +25,16 @@#include <linux/memcontrol.h> #include <linux/mm_inline.h> #include <linux/secretmem.h> +#include <linux/qpw.h> #include "internal.h" struct mlock_fbatch { -local_lock_t lock; +qpw_lock_t lock;struct folio_batch fbatch; }; -static DEFINE_PER_CPU(struct mlock_fbatch, mlock_fbatch) = { - .lock =INIT_LOCAL_LOCK (lock), -}; +static DEFINE_PER_CPU(struct mlock_fbatch, mlock_fbatch);bool can_do_mlock (void ) {
@@ -209,18 +208,25 @@ static void mlock_folio_batch(struct folfolios_put (fbatch); } -void mlock_drain_local (void ) +void mlock_drain_cpu (int cpu) {struct folio_batch *fbatch; -local_lock (&mlock_fbatch.lock); - fbatch =this_cpu_ptr (&mlock_fbatch.fbatch); +qpw_lock (&mlock_fbatch.lock, cpu); + fbatch =per_cpu_ptr (&mlock_fbatch.fbatch, cpu);if (folio_batch_count (fbatch))mlock_folio_batch (fbatch); -local_unlock (&mlock_fbatch.lock); +qpw_unlock (&mlock_fbatch.lock, cpu); } -void mlock_drain_remote (int cpu) +void mlock_drain_local (void ) +{ +migrate_disable (); +mlock_drain_cpu (smp_processor_id ()); +migrate_enable (); +} + +void mlock_drain_offline (int cpu) {struct folio_batch *fbatch;
@@ -242,9 +248,12 @@ bool need_mlock_drain(int cpu)void mlock_folio (struct folio *folio) {struct folio_batch *fbatch; +int cpu; -local_lock (&mlock_fbatch.lock); - fbatch =this_cpu_ptr (&mlock_fbatch.fbatch); +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&mlock_fbatch.lock, cpu); + fbatch =per_cpu_ptr (&mlock_fbatch.fbatch, cpu);if (!folio_test_set_mlocked (folio)) {int nr_pages =folio_nr_pages (folio);
@@ -257,7 +266,8 @@ void mlock_folio(struct folio *folio)if (!folio_batch_add (fbatch,mlock_lru (folio)) || !folio_may_be_lru_cached (folio) ||lru_cache_disabled ())mlock_folio_batch (fbatch); -local_unlock (&mlock_fbatch.lock); +qpw_unlock (&mlock_fbatch.lock, cpu); +migrate_enable (); }/**
@@ -268,9 +278,13 @@ void mlock_new_folio(struct folio *folio {struct folio_batch *fbatch;int nr_pages =folio_nr_pages (folio); +int cpu; + +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&mlock_fbatch.lock, cpu); -local_lock (&mlock_fbatch.lock); - fbatch =this_cpu_ptr (&mlock_fbatch.fbatch); + fbatch =per_cpu_ptr (&mlock_fbatch.fbatch, cpu);folio_set_mlocked (folio);zone_stat_mod_folio (folio,NR_MLOCK , nr_pages);
@@ -280,7 +294,8 @@ void mlock_new_folio(struct folio *folioif (!folio_batch_add (fbatch,mlock_new (folio)) || !folio_may_be_lru_cached (folio) ||lru_cache_disabled ())mlock_folio_batch (fbatch); -local_unlock (&mlock_fbatch.lock); +migrate_enable (); +qpw_unlock (&mlock_fbatch.lock, cpu); }/**
@@ -290,9 +305,13 @@ void mlock_new_folio(struct folio *foliovoid munlock_folio (struct folio *folio) {struct folio_batch *fbatch; +int cpu; -local_lock (&mlock_fbatch.lock); - fbatch =this_cpu_ptr (&mlock_fbatch.fbatch); +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&mlock_fbatch.lock, cpu); + + fbatch =per_cpu_ptr (&mlock_fbatch.fbatch, cpu);/* * folio_test_clear_mlocked(folio) must be left to __munlock_folio(), * which will check whether the folio is multiply mlocked.
@@ -301,7 +320,8 @@ void munlock_folio(struct folio *folio)if (!folio_batch_add (fbatch, folio) || !folio_may_be_lru_cached (folio) ||lru_cache_disabled ())mlock_folio_batch (fbatch); -local_unlock (&mlock_fbatch.lock); +qpw_unlock (&mlock_fbatch.lock, cpu); +migrate_enable (); } static inline unsigned int folio_mlock_step(struct folio *folio,
@@ -823,3 +843,18 @@ void user_shm_unlock(size_t size, structspin_unlock (&shmlock_user_lock);put_ucounts (ucounts); } + +int __init mlock_init (void ) +{ +unsigned int cpu; + +for_each_possible_cpu (cpu) { +struct mlock_fbatch *fbatch = &per_cpu (mlock_fbatch, cpu); + +qpw_lock_init (&fbatch->lock); + } + +return 0 ; +} + +module_init (mlock_init);
mm/swap.c +51 -39
202 lines not on this page · raw message
mm/internal.h +3 -1
@@ -1061,10 +1061,12 @@ static inline void munlock_vma_folio(strmunlock_folio (folio); } +int __init mlock_init (void );void mlock_new_folio (struct folio *folio);bool need_mlock_drain (int cpu);void mlock_drain_local (void ); -void mlock_drain_remote (int cpu); +void mlock_drain_cpu (int cpu); +void mlock_drain_offline (int cpu);extern pmd_t maybe_pmd_mkwrite (pmd_t pmd,struct vm_area_struct *vma);
mm/page_alloc.c +1 -1
@@ -6251,7 +6251,7 @@ static int page_alloc_cpu_dead(unsignedstruct zone *zone;lru_add_drain_cpu (cpu); -mlock_drain_remote (cpu); +mlock_drain_offline (cpu);drain_pages (cpu);/*
Re: [PATCH 3/4] swap: apply new queue_percpu_work_on() interface
Quoted 8 lines · Marcelo Tosatti
Make use of the new qpw_{un,}lock*() and
queue_percpu_work_on()interface to improve performance & latency onPREEMPT_RTkernels.For functions that may be scheduled in a different cpu, replace local_{un,}lock*() by qpw_{un,}lock*(), and replace
schedule_work_on()byqueue_percpu_work_on(). The same happens forflush_work()andflush_percpu_work().
Quoted 4 lines
The change requires allocation of
qpw_structsinstead of awork_structs, and changing parameters of a few functions to include the cpu parameter.This should bring no relevant performance impact on non-RT kernels:
I think this is still referencing the previuos version, as there may be impact in PREEMPT_RT=n kernels if QPW=y and qpw=1 in kernel cmdline.
I would go with:
This should bring no relevant performance impact on non-QPW kernels
Quoted 70 lines
For functions that may be scheduled in a different cpu, the local_*lock's
this_cpu_ptr()becomes a per_cpu_ptr(smp_processor_id()).Signed-off-by Leonardo Bras <[email protected]>Signed-off-by Marcelo Tosatti <[email protected]>--- mm/internal.h | 4 +- mm/mlock.c | 71 ++++++++++++++++++++++++++++++++------------ mm/page_alloc.c | 2 - mm/swap.c | 90 +++++++++++++++++++++++++++++++------------------------- 4 files changed, 108 insertions(+), 59 deletions(-)Index:
slab/mm/mlock.c
===================================================================mm/mlock.c +15
-9@@ -25,17 +25,16 @@#include <linux/memcontrol.h> #include <linux/mm_inline.h> #include <linux/secretmem.h> +#include <linux/qpw.h> #include "internal.h" struct mlock_fbatch { -local_lock_t lock; +qpw_lock_t lock;struct folio_batch fbatch; }; -static DEFINE_PER_CPU(struct mlock_fbatch, mlock_fbatch) = { - .lock =INIT_LOCAL_LOCK (lock), -}; +static DEFINE_PER_CPU(struct mlock_fbatch, mlock_fbatch);bool can_do_mlock (void ) {@@ -209,18 +208,25 @@ static void mlock_folio_batch(struct folfolios_put (fbatch); } -void mlock_drain_local (void ) +void mlock_drain_cpu (int cpu) {struct folio_batch *fbatch; -local_lock (&mlock_fbatch.lock); - fbatch =this_cpu_ptr (&mlock_fbatch.fbatch); +qpw_lock (&mlock_fbatch.lock, cpu); + fbatch =per_cpu_ptr (&mlock_fbatch.fbatch, cpu);if (folio_batch_count (fbatch))mlock_folio_batch (fbatch); -local_unlock (&mlock_fbatch.lock); +qpw_unlock (&mlock_fbatch.lock, cpu); } -void mlock_drain_remote (int cpu) +void mlock_drain_local (void ) +{ +migrate_disable (); +mlock_drain_cpu (smp_processor_id ()); +migrate_enable (); +} + +void mlock_drain_offline (int cpu) {struct folio_batch *fbatch;
mm/mlock.c bool need_mlock_drain(int cpu) · line 254
Quoted 9 lines
mm/mlock.c +3
-2@@ -242,9 +248,12 @@ bool need_mlock_drain(int cpu)void mlock_folio (struct folio *folio) {struct folio_batch *fbatch; +int cpu; -local_lock (&mlock_fbatch.lock); - fbatch =this_cpu_ptr (&mlock_fbatch.fbatch); +migrate_disable (); + cpu =smp_processor_id ();
Wondering if for these cases it would make sense to have something like:
qpw_get_local_cpu() andqpw_put_local_cpu()
so we could encapsulate these migrate_{en,dis}able() and the smp_processor_id().
Or even,
int qpw_local_lock() {
migrate_disable();
cpu = smp_processor_id();
qpw_lock(..., cpu);
return cpu;
}and
qpw_local_unlock(cpu){
qpw_unlock(...,cpu);
migrate_enable();
} so it's more direct to convert the local-only cases.
What do you think?
Quoted 32 lines
mm/mlock.c +10
-3+qpw_lock (&mlock_fbatch.lock, cpu); + fbatch =per_cpu_ptr (&mlock_fbatch.fbatch, cpu);if (!folio_test_set_mlocked (folio)) {int nr_pages =folio_nr_pages (folio);@@ -257,7 +266,8 @@ void mlock_folio(struct folio *folio)if (!folio_batch_add (fbatch,mlock_lru (folio)) || !folio_may_be_lru_cached (folio) ||lru_cache_disabled ())mlock_folio_batch (fbatch); -local_unlock (&mlock_fbatch.lock); +qpw_unlock (&mlock_fbatch.lock, cpu); +migrate_enable (); }/** @@ -268,9 +278,13 @@ void mlock_new_folio(struct folio *folio {struct folio_batch *fbatch;int nr_pages =folio_nr_pages (folio); +int cpu; + +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&mlock_fbatch.lock, cpu); -local_lock (&mlock_fbatch.lock); - fbatch =this_cpu_ptr (&mlock_fbatch.fbatch); + fbatch =per_cpu_ptr (&mlock_fbatch.fbatch, cpu);folio_set_mlocked (folio);zone_stat_mod_folio (folio,NR_MLOCK , nr_pages);
mm/mlock.c void mlock_new_folio(struct folio *folio · line 298
Quoted 6 lines
mm/mlock.c +2
-1@@ -280,7 +294,8 @@ void mlock_new_folio(struct folio *folioif (!folio_batch_add (fbatch,mlock_new (folio)) || !folio_may_be_lru_cached (folio) ||lru_cache_disabled ())mlock_folio_batch (fbatch); -local_unlock (&mlock_fbatch.lock); +migrate_enable (); +qpw_unlock (&mlock_fbatch.lock, cpu);
in the above conversion, the migrate_enable() happened after qpw_unlock, and in this one is the oposite. Any particular reason?
Quoted 88 lines
mm/mlock.c +23
-3}/** @@ -290,9 +305,13 @@ void mlock_new_folio(struct folio *foliovoid munlock_folio (struct folio *folio) {struct folio_batch *fbatch; +int cpu; -local_lock (&mlock_fbatch.lock); - fbatch =this_cpu_ptr (&mlock_fbatch.fbatch); +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&mlock_fbatch.lock, cpu); + + fbatch =per_cpu_ptr (&mlock_fbatch.fbatch, cpu);/* * folio_test_clear_mlocked(folio) must be left to __munlock_folio(), * which will check whether the folio is multiply mlocked. @@ -301,7 +320,8 @@ void munlock_folio(struct folio *folio)if (!folio_batch_add (fbatch, folio) || !folio_may_be_lru_cached (folio) ||lru_cache_disabled ())mlock_folio_batch (fbatch); -local_unlock (&mlock_fbatch.lock); +qpw_unlock (&mlock_fbatch.lock, cpu); +migrate_enable (); } static inline unsigned int folio_mlock_step(struct folio *folio,@@ -823,3 +843,18 @@ void user_shm_unlock(size_t size, structspin_unlock (&shmlock_user_lock);put_ucounts (ucounts); } + +int __init mlock_init (void ) +{ +unsigned int cpu; + +for_each_possible_cpu (cpu) { +struct mlock_fbatch *fbatch = &per_cpu (mlock_fbatch, cpu); + +qpw_lock_init (&fbatch->lock); + } + +return 0 ; +} + +module_init (mlock_init);Index:
slab/mm/swap.c
===================================================================mm/swap.c +4
-7@@ -35,7 +35,7 @@#include <linux/uio.h> #include <linux/hugetlb.h> #include <linux/page_idle.h> -#include <linux/local_lock.h> +#include <linux/qpw.h> #include <linux/buffer_head.h> #include "internal.h" @@ -52,7 +52,7 @@ struct cpu_fbatches {* The following folio batches are grouped together because they are protected * by disabling preemption (and interrupts remain enabled). */ -local_lock_t lock; +qpw_lock_t lock;struct folio_batch lru_add;struct folio_batch lru_deactivate_file;struct folio_batch lru_deactivate;@@ -61,14 +61,11 @@ struct cpu_fbatches {struct folio_batch lru_activate;#endif /* Protecting the following batches which require disabling interrupts */ -local_lock_t lock_irq; +qpw_lock_t lock_irq;struct folio_batch lru_move_tail; }; -static DEFINE_PER_CPU(struct cpu_fbatches, cpu_fbatches) = { - .lock =INIT_LOCAL_LOCK (lock), - .lock_irq =INIT_LOCAL_LOCK (lock_irq), -}; +static DEFINE_PER_CPU(struct cpu_fbatches, cpu_fbatches);static void __page_cache_release (struct folio *folio,struct lruvec **lruvecp,unsigned long *flagsp)
mm/swap.c static void __folio_batch_add_and_move(s · line 185
Quoted 6 lines
mm/swap.c +1
-0@@ -183,22 +180,24 @@ static void __folio_batch_add_and_move(s struct folio *folio, move_fn_t move_fn, bool disable_irq) { unsigned long flags; +int cpu;folio_get (folio);
don't we need the migrate_disable() here?
Quoted 25 lines
mm/swap.c +7
-6+ cpu =smp_processor_id ();if (disable_irq) -local_lock_irqsave (&cpu_fbatches.lock_irq, flags); +qpw_lock_irqsave (&cpu_fbatches.lock_irq, flags, cpu);else -local_lock (&cpu_fbatches.lock); +qpw_lock (&cpu_fbatches.lock, cpu); -if (!folio_batch_add (this_cpu_ptr (fbatch), folio) || +if (!folio_batch_add (per_cpu_ptr (fbatch, cpu), folio) || !folio_may_be_lru_cached (folio) ||lru_cache_disabled ()) -folio_batch_move_lru (this_cpu_ptr (fbatch), move_fn); +folio_batch_move_lru (per_cpu_ptr (fbatch, cpu), move_fn);if (disable_irq) -local_unlock_irqrestore (&cpu_fbatches.lock_irq, flags); +qpw_unlock_irqrestore (&cpu_fbatches.lock_irq, flags, cpu);else -local_unlock (&cpu_fbatches.lock); +qpw_unlock (&cpu_fbatches.lock, cpu); }#define folio_batch_add_and_move (folio, op) \
mm/swap.c static void __lru_cache_activate_folio(s · line 359
Quoted 3 lines
mm/swap.c +0
-0@@ -358,9 +357,10 @@ static void __lru_cache_activate_folio(s {struct folio_batch *fbatch;int i;
and here?
Quoted 31 lines
mm/swap.c +6
-5+int cpu =smp_processor_id (); -local_lock (&cpu_fbatches.lock); - fbatch =this_cpu_ptr (&cpu_fbatches.lru_add); +qpw_lock (&cpu_fbatches.lock, cpu); + fbatch =per_cpu_ptr (&cpu_fbatches.lru_add, cpu);/* * Search backwards on the optimistic assumption that the folio being @@ -381,7 +381,7 @@ static void __lru_cache_activate_folio(s } } -local_unlock (&cpu_fbatches.lock); +qpw_unlock (&cpu_fbatches.lock, cpu); }#ifdef CONFIG_LRU_GEN @@ -653,9 +653,9 @@ void lru_add_drain_cpu(int cpu)unsigned long flags;/* No harm done if a racing interrupt already did this */ -local_lock_irqsave (&cpu_fbatches.lock_irq, flags); +qpw_lock_irqsave (&cpu_fbatches.lock_irq, flags, cpu);folio_batch_move_lru (fbatch, lru_move_tail); -local_unlock_irqrestore (&cpu_fbatches.lock_irq, flags); +qpw_unlock_irqrestore (&cpu_fbatches.lock_irq, flags, cpu); } fbatch = &fbatches->lru_deactivate_file;
mm/swap.c void folio_mark_lazyfree(struct folio *f · line 739
Quoted 7 lines
mm/swap.c +0
-4@@ -733,10 +733,12 @@ void folio_mark_lazyfree(struct folio *fvoid lru_add_drain (void ) { -local_lock (&cpu_fbatches.lock); -lru_add_drain_cpu (smp_processor_id ()); -local_unlock (&cpu_fbatches.lock); -mlock_drain_local ();
and here?
Quoted 20 lines
mm/swap.c +7
-5+int cpu =smp_processor_id (); + +qpw_lock (&cpu_fbatches.lock, cpu); +lru_add_drain_cpu (cpu); +qpw_unlock (&cpu_fbatches.lock, cpu); +mlock_drain_cpu (cpu); }/* @@ -745,30 +747,32 @@ void lru_add_drain(void)* the same cpu. It shouldn't be a problem in !SMP case since * the core is only one and the locks will disable preemption. */ -static void lru_add_mm_drain (void ) +static void lru_add_mm_drain (int cpu) { -local_lock (&cpu_fbatches.lock); -lru_add_drain_cpu (smp_processor_id ()); -local_unlock (&cpu_fbatches.lock); -mlock_drain_local ();
mm/swap.c void lru_add_drain(void) · line 759
Quoted 10 lines
mm/swap.c +4
-2+qpw_lock (&cpu_fbatches.lock, cpu); +lru_add_drain_cpu (cpu); +qpw_unlock (&cpu_fbatches.lock, cpu); +mlock_drain_cpu (cpu); }void lru_add_drain_cpu_zone (struct zone *zone) { -local_lock (&cpu_fbatches.lock); -lru_add_drain_cpu (smp_processor_id ());
and here ?
Quoted 50 lines
mm/swap.c +13
-9+int cpu =smp_processor_id (); + +qpw_lock (&cpu_fbatches.lock, cpu); +lru_add_drain_cpu (cpu);drain_local_pages (zone); -local_unlock (&cpu_fbatches.lock); -mlock_drain_local (); +qpw_unlock (&cpu_fbatches.lock, cpu); +mlock_drain_cpu (cpu); }#ifdef CONFIG_SMP -static DEFINE_PER_CPU(struct work_struct, lru_add_drain_work); +static DEFINE_PER_CPU(struct qpw_struct, lru_add_drain_qpw); -static void lru_add_drain_per_cpu (struct work_struct *dummy) +static void lru_add_drain_per_cpu (struct work_struct *w) { -lru_add_mm_drain (); +lru_add_mm_drain (qpw_get_cpu (w)); } static DEFINE_PER_CPU(struct work_struct, bh_add_drain_work);@@ -883,12 +887,12 @@ static inline void __lru_add_drain_all(bcpumask_clear (&has_mm_work);cpumask_clear (&has_bh_work);for_each_online_cpu (cpu) { -struct work_struct *mm_work = &per_cpu (lru_add_drain_work, cpu); +struct qpw_struct *mm_qpw = &per_cpu (lru_add_drain_qpw, cpu);struct work_struct *bh_work = &per_cpu (bh_add_drain_work, cpu);if (cpu_needs_mm_drain (cpu)) { -INIT_WORK (mm_work, lru_add_drain_per_cpu); -queue_work_on (cpu, mm_percpu_wq, mm_work); +INIT_QPW (mm_qpw, lru_add_drain_per_cpu, cpu); +queue_percpu_work_on (cpu, mm_percpu_wq, mm_qpw);__cpumask_set_cpu (cpu, &has_mm_work); }@@ -900,7 +904,7 @@ static inline void __lru_add_drain_all(b }for_each_cpu (cpu, &has_mm_work) -flush_work (&per_cpu (lru_add_drain_work, cpu)); +flush_percpu_work (&per_cpu (lru_add_drain_qpw, cpu));for_each_cpu (cpu, &has_bh_work)flush_work (&per_cpu (bh_add_drain_work, cpu));
mm/swap.c void lru_cache_disable(void) · line 953
Quoted 4 lines
mm/swap.c +0
-1@@ -950,7 +954,7 @@ void lru_cache_disable(void)#ifdef CONFIG_SMP __lru_add_drain_all (true);#else -lru_add_mm_drain ();
and here, I wonder
Quoted 49 lines
mm/swap.c +9
-0+lru_add_mm_drain (smp_processor_id ());invalidate_bh_lrus_cpu ();#endif }@@ -1124,6 +1128,7 @@ static const struct ctl_table swap_sysctvoid __init swap_setup (void ) {unsigned long megs =PAGES_TO_MB (totalram_pages ()); +unsigned int cpu;/* Use a smaller cluster for small-memory machines */ if (megs <16 )@@ -1136,4 +1141,11 @@ void __init swap_setup(void)*/ register_sysctl_init ("vm" , swap_sysctl_table); + +for_each_possible_cpu (cpu) { +struct cpu_fbatches *fbatches = &per_cpu (cpu_fbatches, cpu); + +qpw_lock_init (&fbatches->lock); +qpw_lock_init (&fbatches->lock_irq); + } }Index:
slab/mm/internal.h
===================================================================mm/internal.h +3
-1@@ -1061,10 +1061,12 @@ static inline void munlock_vma_folio(strmunlock_folio (folio); } +int __init mlock_init (void );void mlock_new_folio (struct folio *folio);bool need_mlock_drain (int cpu);void mlock_drain_local (void ); -void mlock_drain_remote (int cpu); +void mlock_drain_cpu (int cpu); +void mlock_drain_offline (int cpu);extern pmd_t maybe_pmd_mkwrite (pmd_t pmd,struct vm_area_struct *vma);Index:
slab/mm/page_alloc.c
===================================================================
mm/page_alloc.c static int page_alloc_cpu_dead(unsigned · line 6257
Quoted 8 lines
mm/page_alloc.c +1
-1@@ -6251,7 +6251,7 @@ static int page_alloc_cpu_dead(unsignedstruct zone *zone;lru_add_drain_cpu (cpu); -mlock_drain_remote (cpu); +mlock_drain_offline (cpu);drain_pages (cpu);/*
TBH, I am still trying to understand if we need the migrate_{en,dis}able():
- There is a data dependency beween cpu being filled and being used.
- If we get the cpu, and then migrate to a different cpu, the operation will still be executed with the data from that starting cpu
- But maybe the compiler tries to optize this because the processor number can be on a register and of easy access, which would break this.
Maybe a READ_ONCE() on smp_processor_id() should suffice?
Other than that, all the conversions done look correct.
That being said, I understand very little about mm code, so let's hope we get proper feedback from those who do :)
Thanks!
Leo
Re: [PATCH 3/4] swap: apply new queue_percpu_work_on() interface
Quoted 22 lines · Leonardo Bras
Review
Quoted 1 line
(quoted diff) +1
-0+ cpu = smp_processor_id();Wondering if for these cases it would make sense to have something like:
qpw_get_local_cpu()andqpw_put_local_cpu()so we could encapsulate these migrate_{en,dis}able() and the
smp_processor_id().Or even,
int qpw_local_lock() { migrate_disable(); cpu = smp_processor_id(); qpw_lock(..., cpu); return cpu; }and
Quoted 8 lines
qpw_local_unlock(cpu){ qpw_unlock(...,cpu); migrate_enable(); }so it's more direct to convert the local-only cases.
What do you think?
Switched to local_qpw_lock variants.
Quoted 5 lines
Review
Quoted 3 lines
{
(quoted diff) +0
-2- local_lock(&cpu_fbatches.lock); - lru_add_drain_cpu(smp_processor_id());and here ?
Fixed lack of migrate_disable/migrate_enable, thanks!
Quoted 7 lines
mm/swap.c void lru_cache_disable(void) · line 953
Quoted 5 lines
mm/swap.c +0
-1@@ -950,7 +954,7 @@ void lru_cache_disable(void)#ifdef CONFIG_SMP __lru_add_drain_all (true);#else -lru_add_mm_drain ();and here, I wonder
This is !CONFIG_SMP, so smp_processor_id is always 0.
Quoted 10 lines
mm/page_alloc.c static int page_alloc_cpu_dead(unsigned · line 6257
Quoted 5 lines
mm/page_alloc.c +0
-0drain_pages (cpu);/* TBH, I am still trying to understand if we need the migrate_{en,dis}able():
- There is a data dependency beween cpu being filled and being used.
- If we get the cpu, and then migrate to a different cpu, the operation will still be executed with the data from that starting cpu
Yes, but on a remote CPU. What prevents the original CPU from accessing its per-CPU local data, therefore racing with the code executing on the remote CPU.
Quoted 14 lines
- But maybe the compiler tries to optize this because the processor number can be on a register and of easy access, which would break this.
Maybe a
READ_ONCE()onsmp_processor_id()should suffice?Other than that, all the conversions done look correct.
That being said, I understand very little about mm code, so let's hope we get proper feedback from those who do :)
Thanks!
Leo
Re: [PATCH 3/4] swap: apply new queue_percpu_work_on() interface
Quoted 58 lines · Marcelo Tosatti
Quoted 22 lines · Leonardo Bras
Review
Quoted 1 line
(quoted diff) +1
-0+ cpu = smp_processor_id();Wondering if for these cases it would make sense to have something like:
qpw_get_local_cpu()andqpw_put_local_cpu()so we could encapsulate these migrate_{en,dis}able() and the
smp_processor_id().Or even,
int qpw_local_lock() { migrate_disable(); cpu = smp_processor_id(); qpw_lock(..., cpu); return cpu; }and
Quoted 8 lines
qpw_local_unlock(cpu){ qpw_unlock(...,cpu); migrate_enable(); }so it's more direct to convert the local-only cases.
What do you think?
Switched to
local_qpw_lockvariants.Quoted 5 lines
Review
Quoted 3 lines
{
(quoted diff) +0
-2- local_lock(&cpu_fbatches.lock); - lru_add_drain_cpu(smp_processor_id());and here ?
Fixed lack of migrate_disable/migrate_enable, thanks!
Quoted 7 lines
mm/swap.c void lru_cache_disable(void) · line 953
Quoted 5 lines
mm/swap.c +0
-1@@ -950,7 +954,7 @@ void lru_cache_disable(void)#ifdef CONFIG_SMP __lru_add_drain_all (true);#else -lru_add_mm_drain ();and here, I wonder
This is !CONFIG_SMP, so
smp_processor_idis always 0.Quoted 6 lines
Quoted 5 lines
mm/page_alloc.c +0
-0drain_pages (cpu);/*
Quoted 8 lines
Quoted 4 lines
TBH, I am still trying to understand if we need the migrate_{en,dis}able():
- There is a data dependency beween cpu being filled and being used.
- If we get the cpu, and then migrate to a different cpu, the operation will still be executed with the data from that starting cpuYes, but on a remote CPU. What prevents the original CPU from accessing its per-CPU local data, therefore racing with the code executing on the remote CPU.
Yes, but really rarely, and contention even more rare.
It also should not break anything, as locking will make sure access to that per-cpu value is being serialized.
I was wondering on the extra cost of migrate_* on the hot path being an undesired effect for other code evaluating switching to QPW. But maybe it's not that bad, and we get rid of that very rare contention.
Thanks!
Leo
Quoted 16 lines
Quoted 15 lines
- But maybe the compiler tries to optize this because the processor number can be on a register and of easy access, which would break this.
Maybe a
READ_ONCE()onsmp_processor_id()should suffice?Other than that, all the conversions done look correct.
That being said, I understand very little about mm code, so let's hope we get proper feedback from those who do :)
Thanks!
Leo
PATCH 4/4 slub: apply new queue_percpu_work_on() interface
In reply to Marcelo TosattiMake use of the new qpw_{un,}lock*() and queue_percpu_work_on() interface to improve performance & latency on PREEMPT_RT kernels.
For functions that may be scheduled in a different cpu, replace local_{un,}lock*() by qpw_{un,}lock*(), and replace schedule_work_on() by queue_percpu_work_on(). The same happens for flush_work() and flush_percpu_work().
This change requires allocation of qpw_structs instead of a work_structs, and changing parameters of a few functions to include the cpu parameter.
This should bring no relevant performance impact on non-RT kernels:
For functions that may be scheduled in a different cpu, the local_*lock's this_cpu_ptr() becomes a per_cpu_ptr(smp_processor_id()).
--- mm/slub.c | 218 ++++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 142 insertions(+), 76 deletions(-)
mm/slub.c +142 -76
@@ -49,6 +49,7 @@#include <linux/irq_work.h> #include <linux/kprobes.h> #include <linux/debugfs.h> +#include <linux/qpw.h> #include <trace/events/kmem.h> #include "internal.h"
@@ -128,7 +129,7 @@* For debug caches, all allocations are forced to go through a list_lock * protected region to serialize against concurrent validation. * -* cpu_sheaves->lock (local_trylock) +* cpu_sheaves->lock (qpw_trylock) * * This lock protects fastpath operations on the percpu sheaves. On !RT it * only disables preemption and does no atomic operations. As long as the main
@@ -156,7 +157,7 @@* Interrupts are disabled as part of list_lock or barn lock operations, or * around the slab_lock operation, in order to make the slab allocator safe * to use in the context of an irq. -* Preemption is disabled as part of local_trylock operations. +* Preemption is disabled as part of qpw_trylock operations. * kmalloc_nolock() and kfree_nolock() are safe in NMI context but see * their limitations. *
@@ -417,7 +418,7 @@ struct slab_sheaf { };struct slub_percpu_sheaves { -local_trylock_t lock; +qpw_trylock_t lock;struct slab_sheaf *main;/* never NULL when unlocked */ struct slab_sheaf *spare;/* empty or full, may be NULL */ struct slab_sheaf *rcu_free;/* for batching kfree_rcu() */
@@ -479,7 +480,7 @@ static nodemask_t slab_nodes;static struct workqueue_struct *flushwq;struct slub_flush_work { -struct work_struct work; +struct qpw_struct qpw;struct kmem_cache *s;bool skip; };
@@ -2826,7 +2827,7 @@ static void __kmem_cache_free_bulk(struc* * returns true if at least partially flushed */ -static bool sheaf_flush_main (struct kmem_cache *s) +static bool sheaf_flush_main (struct kmem_cache *s,int cpu) {struct slub_percpu_sheaves *pcs;unsigned int batch, remaining;
@@ -2835,10 +2836,10 @@ static bool sheaf_flush_main(struct kmembool ret = false; next_batch: -if (!local_trylock (&s->cpu_sheaves->lock)) +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu))return ret; - pcs =this_cpu_ptr (s->cpu_sheaves); + pcs =per_cpu_ptr (s->cpu_sheaves, cpu); sheaf = pcs->main; batch =min (PCS_BATCH_MAX , sheaf->size);
@@ -2848,7 +2849,7 @@ next_batch: remaining = sheaf->size; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu);__kmem_cache_free_bulk (s, batch, &objects[0 ]);
@@ -2932,13 +2933,13 @@ static void rcu_free_sheaf_nobarn(struct* flushing operations are rare so let's keep it simple and flush to slabs * directly, skipping the barn */ -static void pcs_flush_all (struct kmem_cache *s) +static void pcs_flush_all (struct kmem_cache *s,int cpu) {struct slub_percpu_sheaves *pcs;struct slab_sheaf *spare, *rcu_free; -local_lock (&s->cpu_sheaves->lock); - pcs =this_cpu_ptr (s->cpu_sheaves); +qpw_lock (&s->cpu_sheaves->lock, cpu); + pcs =per_cpu_ptr (s->cpu_sheaves, cpu); spare = pcs->spare; pcs->spare =NULL ;
@@ -2946,7 +2947,7 @@ static void pcs_flush_all(struct kmem_ca rcu_free = pcs->rcu_free; pcs->rcu_free =NULL ; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu);if (spare) {sheaf_flush_unused (s, spare);
@@ -2956,7 +2957,7 @@ static void pcs_flush_all(struct kmem_caif (rcu_free)call_rcu (&rcu_free->rcu_head, rcu_free_sheaf_nobarn); -sheaf_flush_main (s); +sheaf_flush_main (s, cpu); }static void __pcs_flush_all_cpu (struct kmem_cache *s,unsigned int cpu)
@@ -3881,13 +3882,13 @@ static void flush_cpu_sheaves(struct wor {struct kmem_cache *s;struct slub_flush_work *sfw; +int cpu =qpw_get_cpu (w); - sfw = container_of(w, struct slub_flush_work, work); - + sfw = &per_cpu (slub_flush, cpu); s = sfw->s;if (cache_has_sheaves (s)) -pcs_flush_all (s); +pcs_flush_all (s, cpu); }static void flush_all_cpus_locked (struct kmem_cache *s)
@@ -3904,17 +3905,17 @@ static void flush_all_cpus_locked(struct sfw->skip = true;continue ; } -INIT_WORK (&sfw->work, flush_cpu_sheaves); +INIT_QPW (&sfw->qpw, flush_cpu_sheaves, cpu); sfw->skip = false; sfw->s = s; -queue_work_on (cpu, flushwq, &sfw->work); +queue_percpu_work_on (cpu, flushwq, &sfw->qpw); }for_each_online_cpu (cpu) { sfw = &per_cpu (slub_flush, cpu);if (sfw->skip)continue ; -flush_work (&sfw->work); +flush_percpu_work (&sfw->qpw); }mutex_unlock (&flush_lock);
@@ -3933,17 +3934,18 @@ static void flush_rcu_sheaf(struct work_struct slab_sheaf *rcu_free;struct slub_flush_work *sfw;struct kmem_cache *s; +int cpu =qpw_get_cpu (w); - sfw = container_of(w, struct slub_flush_work, work); + sfw = &per_cpu (slub_flush, cpu); s = sfw->s; -local_lock (&s->cpu_sheaves->lock); - pcs =this_cpu_ptr (s->cpu_sheaves); +qpw_lock (&s->cpu_sheaves->lock, cpu); + pcs =per_cpu_ptr (s->cpu_sheaves, cpu); rcu_free = pcs->rcu_free; pcs->rcu_free =NULL ; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu);if (rcu_free)call_rcu (&rcu_free->rcu_head, rcu_free_sheaf_nobarn);
@@ -3968,14 +3970,14 @@ void flush_rcu_sheaves_on_cache(struct k* sure the __kfree_rcu_sheaf() finished its call_rcu() */ -INIT_WORK (&sfw->work, flush_rcu_sheaf); +INIT_QPW (&sfw->qpw, flush_rcu_sheaf, cpu); sfw->s = s; -queue_work_on (cpu, flushwq, &sfw->work); +queue_percpu_work_on (cpu, flushwq, &sfw->qpw); }for_each_online_cpu (cpu) { sfw = &per_cpu (slub_flush, cpu); -flush_work (&sfw->work); +flush_percpu_work (&sfw->qpw); }mutex_unlock (&flush_lock);
@@ -4472,22 +4474,24 @@ bool slab_post_alloc_hook(struct kmem_ca* * Must be called with the cpu_sheaves local lock locked. If successful, returns * the pcs pointer and the local lock locked (possibly on a different cpu than -* initially called). If not successful, returns NULL and the local lock -* unlocked. +* initially called), and migration disabled. If not successful, returns NULL +* and the local lock unlocked, with migration enabled. */ static struct slub_percpu_sheaves * -__pcs_replace_empty_main (struct kmem_cache *s,struct slub_percpu_sheaves *pcs,gfp_t gfp) +__pcs_replace_empty_main (struct kmem_cache *s,struct slub_percpu_sheaves *pcs,gfp_t gfp, +int *cpu) {struct slab_sheaf *empty =NULL ;struct slab_sheaf *full;struct node_barn *barn;bool can_alloc; -lockdep_assert_held (this_cpu_ptr (&s->cpu_sheaves->lock)); +qpw_lockdep_assert_held (&s->cpu_sheaves->lock);/* Bootstrap or debug cache, back off */ if (unlikely (!cache_has_sheaves (s))) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();return NULL ; }
@@ -4498,7 +4502,8 @@ __pcs_replace_empty_main(struct kmem_cac barn =get_barn (s);if (!barn) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();return NULL ; }
@@ -4524,7 +4529,8 @@ __pcs_replace_empty_main(struct kmem_cac } } -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();if (!can_alloc)return NULL ;
@@ -4550,7 +4556,9 @@ __pcs_replace_empty_main(struct kmem_cac* we can reach here only when gfpflags_allow_blocking * so this must not be an irq */ -local_lock (&s->cpu_sheaves->lock); +migrate_disable (); + *cpu =smp_processor_id (); +qpw_lock (&s->cpu_sheaves->lock, *cpu); pcs =this_cpu_ptr (s->cpu_sheaves);/*
@@ -4593,6 +4601,7 @@ void *alloc_from_pcs(struct kmem_cache *struct slub_percpu_sheaves *pcs;bool node_requested;void *object; +int cpu;#ifdef CONFIG_NUMA if (static_branch_unlikely(&strict_numa) &&
@@ -4627,13 +4636,17 @@ void *alloc_from_pcs(struct kmem_cache *return NULL ; } -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) { +migrate_enable ();return NULL ; + } pcs =this_cpu_ptr (s->cpu_sheaves);if (unlikely (pcs->main->size ==0 )) { - pcs =__pcs_replace_empty_main (s, pcs, gfp); + pcs =__pcs_replace_empty_main (s, pcs, gfp, &cpu);if (unlikely (!pcs))return NULL ; }
@@ -4647,7 +4660,8 @@ void *alloc_from_pcs(struct kmem_cache ** the current allocation or previous freeing process. */ if (page_to_nid (virt_to_page (object)) != node) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat (s,ALLOC_NODE_MISMATCH );return NULL ; }
@@ -4655,7 +4669,8 @@ void *alloc_from_pcs(struct kmem_cache * pcs->main->size--; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat (s,ALLOC_FASTPATH );
@@ -4670,10 +4685,15 @@ unsigned int alloc_from_pcs_bulk(structstruct slab_sheaf *main;unsigned int allocated =0 ;unsigned int batch; +int cpu; next_batch: -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) { +migrate_enable ();return allocated; + } pcs =this_cpu_ptr (s->cpu_sheaves);
@@ -4683,7 +4703,8 @@ next_batch:struct node_barn *barn;if (unlikely (!cache_has_sheaves (s))) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();return allocated; }
@@ -4694,7 +4715,8 @@ next_batch: barn =get_barn (s);if (!barn) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();return allocated; }
@@ -4709,7 +4731,8 @@ next_batch:stat (s,BARN_GET_FAIL ); -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();/* * Once full sheaves in barn are depleted, let the bulk
@@ -4727,7 +4750,8 @@ do_alloc: main->size -= batch;memcpy (p, main->objects + main->size, batch *sizeof (void *)); -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat_add (s,ALLOC_FASTPATH , batch);
@@ -4877,6 +4901,7 @@ kmem_cache_prefill_sheaf(struct kmem_cacstruct slub_percpu_sheaves *pcs;struct slab_sheaf *sheaf =NULL ;struct node_barn *barn; +int cpu;if (unlikely (!size))return NULL ;
@@ -4906,7 +4931,9 @@ kmem_cache_prefill_sheaf(struct kmem_cacreturn sheaf; } -local_lock (&s->cpu_sheaves->lock); +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&s->cpu_sheaves->lock, cpu); pcs =this_cpu_ptr (s->cpu_sheaves);if (pcs->spare) {
@@ -4925,7 +4952,8 @@ kmem_cache_prefill_sheaf(struct kmem_cacstat (s,BARN_GET_FAIL ); } -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();if (!sheaf)
@@ -4961,6 +4989,7 @@ void kmem_cache_return_sheaf(struct kmem {struct slub_percpu_sheaves *pcs;struct node_barn *barn; +int cpu;if (unlikely ((sheaf->capacity != s->sheaf_capacity) || sheaf->pfmemalloc)) {
@@ -4969,7 +4998,9 @@ void kmem_cache_return_sheaf(struct kmemreturn ; } -local_lock (&s->cpu_sheaves->lock); +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&s->cpu_sheaves->lock, cpu); pcs =this_cpu_ptr (s->cpu_sheaves); barn =get_barn (s);
@@ -4979,7 +5010,8 @@ void kmem_cache_return_sheaf(struct kmemstat (s,SHEAF_RETURN_FAST ); } -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();if (!sheaf)return ;
@@ -5507,9 +5539,9 @@ slab_empty:*/ static void __pcs_install_empty_sheaf (struct kmem_cache *s,struct slub_percpu_sheaves *pcs,struct slab_sheaf *empty, -struct node_barn *barn) +struct node_barn *barn,int cpu) { -lockdep_assert_held (this_cpu_ptr (&s->cpu_sheaves->lock)); +qpw_lockdep_assert_held (&s->cpu_sheaves->lock);/* This is what we expect to find if nobody interrupted us. */ if (likely (!pcs->spare)) {
@@ -5546,31 +5578,34 @@ static void __pcs_install_empty_sheaf(st/* * Replace the full main sheaf with a (at least partially) empty sheaf. * -* Must be called with the cpu_sheaves local lock locked. If successful, returns -* the pcs pointer and the local lock locked (possibly on a different cpu than -* initially called). If not successful, returns NULL and the local lock -* unlocked. +* Must be called with the cpu_sheaves local lock locked, and migration counter +* increased. If successful, returns the pcs pointer and the local lock locked +* (possibly on a different cpu than initially called), with migration counter +* increased. If not successful, returns NULL and the local lock unlocked, +* and migration counter decreased. */ static struct slub_percpu_sheaves *__pcs_replace_full_main (struct kmem_cache *s,struct slub_percpu_sheaves *pcs, -bool allow_spin) +bool allow_spin,int *cpu) {struct slab_sheaf *empty;struct node_barn *barn;bool put_fail; restart: -lockdep_assert_held (this_cpu_ptr (&s->cpu_sheaves->lock)); +qpw_lockdep_assert_held (&s->cpu_sheaves->lock);/* Bootstrap or debug cache, back off */ if (unlikely (!cache_has_sheaves (s))) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();return NULL ; } barn =get_barn (s);if (!barn) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();return NULL ; }
@@ -5607,7 +5642,8 @@ restart:stat (s,BARN_PUT_FAIL ); pcs->spare =NULL ; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();sheaf_flush_unused (s, to_flush); empty = to_flush;
@@ -5623,7 +5659,8 @@ restart: put_fail = true; alloc_empty: -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();/* * alloc_empty_sheaf() doesn't support !allow_spin and it's
@@ -5640,11 +5677,17 @@ alloc_empty:if (put_fail)stat (s,BARN_PUT_FAIL ); -if (!sheaf_flush_main (s)) +migrate_disable (); + *cpu =smp_processor_id (); +if (!sheaf_flush_main (s, *cpu)) { +migrate_enable ();return NULL ; + } -if (!local_trylock (&s->cpu_sheaves->lock)) +if (!qpw_trylock (&s->cpu_sheaves->lock, *cpu)) { +migrate_enable ();return NULL ; + } pcs =this_cpu_ptr (s->cpu_sheaves);
@@ -5659,13 +5702,14 @@ alloc_empty:return pcs; got_empty: -if (!local_trylock (&s->cpu_sheaves->lock)) { +if (!qpw_trylock (&s->cpu_sheaves->lock, *cpu)) { +migrate_enable ();barn_put_empty_sheaf (barn, empty);return NULL ; } pcs =this_cpu_ptr (s->cpu_sheaves); -__pcs_install_empty_sheaf (s, pcs, empty, barn); +__pcs_install_empty_sheaf (s, pcs, empty, barn, *cpu);return pcs; }
@@ -5678,22 +5722,28 @@ static __fastpath_inlinebool free_to_pcs (struct kmem_cache *s,void *object,bool allow_spin) {struct slub_percpu_sheaves *pcs; +int cpu; -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) { +migrate_enable ();return false; + } pcs =this_cpu_ptr (s->cpu_sheaves);if (unlikely (pcs->main->size == s->sheaf_capacity)) { - pcs =__pcs_replace_full_main (s, pcs, allow_spin); + pcs =__pcs_replace_full_main (s, pcs, allow_spin, &cpu);if (unlikely (!pcs))return false; } pcs->main->objects[pcs->main->size++] = object; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat (s,FREE_FASTPATH );
@@ -5777,14 +5827,19 @@ bool __kfree_rcu_sheaf(struct kmem_cache {struct slub_percpu_sheaves *pcs;struct slab_sheaf *rcu_sheaf; +int cpu;if (WARN_ON_ONCE (IS_ENABLED (CONFIG_PREEMPT_RT )))return false;lock_map_acquire_try (&kfree_rcu_sheaf_map); -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) { +migrate_enable (); goto fail; + } pcs =this_cpu_ptr (s->cpu_sheaves);
@@ -5795,7 +5850,8 @@ bool __kfree_rcu_sheaf(struct kmem_cache/* Bootstrap or debug cache, fall back */ if (unlikely (!cache_has_sheaves (s))) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable (); goto fail; }
@@ -5807,7 +5863,8 @@ bool __kfree_rcu_sheaf(struct kmem_cache barn =get_barn (s);if (!barn) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable (); goto fail; }
@@ -5818,15 +5875,18 @@ bool __kfree_rcu_sheaf(struct kmem_cache goto do_free; } -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable (); empty =alloc_empty_sheaf (s,GFP_NOWAIT );if (!empty) goto fail; -if (!local_trylock (&s->cpu_sheaves->lock)) { +migrate_disable (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) {barn_put_empty_sheaf (barn, empty); +migrate_enable (); goto fail; }
@@ -5862,7 +5922,8 @@ do_free:if (rcu_sheaf)call_rcu (&rcu_sheaf->rcu_head, rcu_free_sheaf); -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat (s,FREE_RCU_SHEAF );lock_map_release (&kfree_rcu_sheaf_map);
@@ -5889,6 +5950,7 @@ static void free_to_pcs_bulk(struct kmemvoid *remote_objects[PCS_BATCH_MAX ];unsigned int remote_nr =0 ;int node =numa_mem_id (); +int cpu; next_remote_batch:while (i < size) {
@@ -5918,7 +5980,9 @@ next_remote_batch: goto flush_remote; next_batch: -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) goto fallback; pcs =this_cpu_ptr (s->cpu_sheaves);
@@ -5961,7 +6025,8 @@ do_free:memcpy (main->objects + main->size, p, batch *sizeof (void *)); main->size += batch; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat_add (s,FREE_FASTPATH , batch);
@@ -5977,7 +6042,8 @@ do_free:return ; no_empty: -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();/* * if we depleted all empty sheaves in the barn or there are too
@@ -7377,7 +7443,7 @@ static int init_percpu_sheaves(struct km pcs =per_cpu_ptr (s->cpu_sheaves, cpu); -local_trylock_init (&pcs->lock); +qpw_trylock_init (&pcs->lock);/* * Bootstrap sheaf has zero size so fast-path allocation fails.
Re: [PATCH 4/4] slub: apply new queue_percpu_work_on() interface
Quoted 8 lines · Marcelo Tosatti
Make use of the new qpw_{un,}lock*() and
queue_percpu_work_on()interface to improve performance & latency onPREEMPT_RTkernels.For functions that may be scheduled in a different cpu, replace local_{un,}lock*() by qpw_{un,}lock*(), and replace
schedule_work_on()byqueue_percpu_work_on(). The same happens forflush_work()andflush_percpu_work().
Quoted 4 lines
This change requires allocation of
qpw_structsinstead of awork_structs, and changing parameters of a few functions to include the cpu parameter.This should bring no relevant performance impact on non-RT kernels:
Same as prev patch
Quoted 440 lines
For functions that may be scheduled in a different cpu, the local_*lock's
this_cpu_ptr()becomes a per_cpu_ptr(smp_processor_id()).Signed-off-by Leonardo Bras <[email protected]>Signed-off-by Marcelo Tosatti <[email protected]>--- mm/slub.c | 218 ++++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 142 insertions(+), 76 deletions(-)Index:
slab/mm/slub.c
===================================================================mm/slub.c +81
-49@@ -49,6 +49,7 @@#include <linux/irq_work.h> #include <linux/kprobes.h> #include <linux/debugfs.h> +#include <linux/qpw.h> #include <trace/events/kmem.h> #include "internal.h" @@ -128,7 +129,7 @@* For debug caches, all allocations are forced to go through a list_lock * protected region to serialize against concurrent validation. * -* cpu_sheaves->lock (local_trylock) +* cpu_sheaves->lock (qpw_trylock) * * This lock protects fastpath operations on the percpu sheaves. On !RT it * only disables preemption and does no atomic operations. As long as the main @@ -156,7 +157,7 @@* Interrupts are disabled as part of list_lock or barn lock operations, or * around the slab_lock operation, in order to make the slab allocator safe * to use in the context of an irq. -* Preemption is disabled as part of local_trylock operations. +* Preemption is disabled as part of qpw_trylock operations. * kmalloc_nolock() and kfree_nolock() are safe in NMI context but see * their limitations. * @@ -417,7 +418,7 @@ struct slab_sheaf { };struct slub_percpu_sheaves { -local_trylock_t lock; +qpw_trylock_t lock;struct slab_sheaf *main;/* never NULL when unlocked */ struct slab_sheaf *spare;/* empty or full, may be NULL */ struct slab_sheaf *rcu_free;/* for batching kfree_rcu() */ @@ -479,7 +480,7 @@ static nodemask_t slab_nodes;static struct workqueue_struct *flushwq;struct slub_flush_work { -struct work_struct work; +struct qpw_struct qpw;struct kmem_cache *s;bool skip; };@@ -2826,7 +2827,7 @@ static void __kmem_cache_free_bulk(struc* * returns true if at least partially flushed */ -static bool sheaf_flush_main (struct kmem_cache *s) +static bool sheaf_flush_main (struct kmem_cache *s,int cpu) {struct slub_percpu_sheaves *pcs;unsigned int batch, remaining;@@ -2835,10 +2836,10 @@ static bool sheaf_flush_main(struct kmembool ret = false; next_batch: -if (!local_trylock (&s->cpu_sheaves->lock)) +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu))return ret; - pcs =this_cpu_ptr (s->cpu_sheaves); + pcs =per_cpu_ptr (s->cpu_sheaves, cpu); sheaf = pcs->main; batch =min (PCS_BATCH_MAX , sheaf->size);@@ -2848,7 +2849,7 @@ next_batch: remaining = sheaf->size; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu);__kmem_cache_free_bulk (s, batch, &objects[0 ]);@@ -2932,13 +2933,13 @@ static void rcu_free_sheaf_nobarn(struct* flushing operations are rare so let's keep it simple and flush to slabs * directly, skipping the barn */ -static void pcs_flush_all (struct kmem_cache *s) +static void pcs_flush_all (struct kmem_cache *s,int cpu) {struct slub_percpu_sheaves *pcs;struct slab_sheaf *spare, *rcu_free; -local_lock (&s->cpu_sheaves->lock); - pcs =this_cpu_ptr (s->cpu_sheaves); +qpw_lock (&s->cpu_sheaves->lock, cpu); + pcs =per_cpu_ptr (s->cpu_sheaves, cpu); spare = pcs->spare; pcs->spare =NULL ;@@ -2946,7 +2947,7 @@ static void pcs_flush_all(struct kmem_ca rcu_free = pcs->rcu_free; pcs->rcu_free =NULL ; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu);if (spare) {sheaf_flush_unused (s, spare);@@ -2956,7 +2957,7 @@ static void pcs_flush_all(struct kmem_caif (rcu_free)call_rcu (&rcu_free->rcu_head, rcu_free_sheaf_nobarn); -sheaf_flush_main (s); +sheaf_flush_main (s, cpu); }static void __pcs_flush_all_cpu (struct kmem_cache *s,unsigned int cpu)@@ -3881,13 +3882,13 @@ static void flush_cpu_sheaves(struct wor {struct kmem_cache *s;struct slub_flush_work *sfw; +int cpu =qpw_get_cpu (w); - sfw = container_of(w, struct slub_flush_work, work); - + sfw = &per_cpu (slub_flush, cpu); s = sfw->s;if (cache_has_sheaves (s)) -pcs_flush_all (s); +pcs_flush_all (s, cpu); }static void flush_all_cpus_locked (struct kmem_cache *s)@@ -3904,17 +3905,17 @@ static void flush_all_cpus_locked(struct sfw->skip = true;continue ; } -INIT_WORK (&sfw->work, flush_cpu_sheaves); +INIT_QPW (&sfw->qpw, flush_cpu_sheaves, cpu); sfw->skip = false; sfw->s = s; -queue_work_on (cpu, flushwq, &sfw->work); +queue_percpu_work_on (cpu, flushwq, &sfw->qpw); }for_each_online_cpu (cpu) { sfw = &per_cpu (slub_flush, cpu);if (sfw->skip)continue ; -flush_work (&sfw->work); +flush_percpu_work (&sfw->qpw); }mutex_unlock (&flush_lock);@@ -3933,17 +3934,18 @@ static void flush_rcu_sheaf(struct work_struct slab_sheaf *rcu_free;struct slub_flush_work *sfw;struct kmem_cache *s; +int cpu =qpw_get_cpu (w); - sfw = container_of(w, struct slub_flush_work, work); + sfw = &per_cpu (slub_flush, cpu); s = sfw->s; -local_lock (&s->cpu_sheaves->lock); - pcs =this_cpu_ptr (s->cpu_sheaves); +qpw_lock (&s->cpu_sheaves->lock, cpu); + pcs =per_cpu_ptr (s->cpu_sheaves, cpu); rcu_free = pcs->rcu_free; pcs->rcu_free =NULL ; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu);if (rcu_free)call_rcu (&rcu_free->rcu_head, rcu_free_sheaf_nobarn);@@ -3968,14 +3970,14 @@ void flush_rcu_sheaves_on_cache(struct k* sure the __kfree_rcu_sheaf() finished its call_rcu() */ -INIT_WORK (&sfw->work, flush_rcu_sheaf); +INIT_QPW (&sfw->qpw, flush_rcu_sheaf, cpu); sfw->s = s; -queue_work_on (cpu, flushwq, &sfw->work); +queue_percpu_work_on (cpu, flushwq, &sfw->qpw); }for_each_online_cpu (cpu) { sfw = &per_cpu (slub_flush, cpu); -flush_work (&sfw->work); +flush_percpu_work (&sfw->qpw); }mutex_unlock (&flush_lock);@@ -4472,22 +4474,24 @@ bool slab_post_alloc_hook(struct kmem_ca* * Must be called with the cpu_sheaves local lock locked. If successful, returns * the pcs pointer and the local lock locked (possibly on a different cpu than -* initially called). If not successful, returns NULL and the local lock -* unlocked. +* initially called), and migration disabled. If not successful, returns NULL +* and the local lock unlocked, with migration enabled. */ static struct slub_percpu_sheaves * -__pcs_replace_empty_main (struct kmem_cache *s,struct slub_percpu_sheaves *pcs,gfp_t gfp) +__pcs_replace_empty_main (struct kmem_cache *s,struct slub_percpu_sheaves *pcs,gfp_t gfp, +int *cpu) {struct slab_sheaf *empty =NULL ;struct slab_sheaf *full;struct node_barn *barn;bool can_alloc; -lockdep_assert_held (this_cpu_ptr (&s->cpu_sheaves->lock)); +qpw_lockdep_assert_held (&s->cpu_sheaves->lock);/* Bootstrap or debug cache, back off */ if (unlikely (!cache_has_sheaves (s))) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();return NULL ; }@@ -4498,7 +4502,8 @@ __pcs_replace_empty_main(struct kmem_cac barn =get_barn (s);if (!barn) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();return NULL ; }@@ -4524,7 +4529,8 @@ __pcs_replace_empty_main(struct kmem_cac } } -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();if (!can_alloc)return NULL ;@@ -4550,7 +4556,9 @@ __pcs_replace_empty_main(struct kmem_cac* we can reach here only when gfpflags_allow_blocking * so this must not be an irq */ -local_lock (&s->cpu_sheaves->lock); +migrate_disable (); + *cpu =smp_processor_id (); +qpw_lock (&s->cpu_sheaves->lock, *cpu); pcs =this_cpu_ptr (s->cpu_sheaves);/* @@ -4593,6 +4601,7 @@ void *alloc_from_pcs(struct kmem_cache *struct slub_percpu_sheaves *pcs;bool node_requested;void *object; +int cpu;#ifdef CONFIG_NUMA if (static_branch_unlikely(&strict_numa) &&@@ -4627,13 +4636,17 @@ void *alloc_from_pcs(struct kmem_cache *return NULL ; } -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) { +migrate_enable ();return NULL ; + } pcs =this_cpu_ptr (s->cpu_sheaves);if (unlikely (pcs->main->size ==0 )) { - pcs =__pcs_replace_empty_main (s, pcs, gfp); + pcs =__pcs_replace_empty_main (s, pcs, gfp, &cpu);if (unlikely (!pcs))return NULL ; }@@ -4647,7 +4660,8 @@ void *alloc_from_pcs(struct kmem_cache ** the current allocation or previous freeing process. */ if (page_to_nid (virt_to_page (object)) != node) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat (s,ALLOC_NODE_MISMATCH );return NULL ; }@@ -4655,7 +4669,8 @@ void *alloc_from_pcs(struct kmem_cache * pcs->main->size--; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat (s,ALLOC_FASTPATH );@@ -4670,10 +4685,15 @@ unsigned int alloc_from_pcs_bulk(structstruct slab_sheaf *main;unsigned int allocated =0 ;unsigned int batch; +int cpu; next_batch: -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) { +migrate_enable ();return allocated; + } pcs =this_cpu_ptr (s->cpu_sheaves);@@ -4683,7 +4703,8 @@ next_batch:struct node_barn *barn;if (unlikely (!cache_has_sheaves (s))) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();return allocated; }@@ -4694,7 +4715,8 @@ next_batch: barn =get_barn (s);if (!barn) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();return allocated; }@@ -4709,7 +4731,8 @@ next_batch:stat (s,BARN_GET_FAIL ); -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();/* * Once full sheaves in barn are depleted, let the bulk @@ -4727,7 +4750,8 @@ do_alloc: main->size -= batch;memcpy (p, main->objects + main->size, batch *sizeof (void *)); -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat_add (s,ALLOC_FASTPATH , batch);@@ -4877,6 +4901,7 @@ kmem_cache_prefill_sheaf(struct kmem_cacstruct slub_percpu_sheaves *pcs;struct slab_sheaf *sheaf =NULL ;struct node_barn *barn; +int cpu;if (unlikely (!size))return NULL ;@@ -4906,7 +4931,9 @@ kmem_cache_prefill_sheaf(struct kmem_cacreturn sheaf; } -local_lock (&s->cpu_sheaves->lock); +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&s->cpu_sheaves->lock, cpu); pcs =this_cpu_ptr (s->cpu_sheaves);if (pcs->spare) {@@ -4925,7 +4952,8 @@ kmem_cache_prefill_sheaf(struct kmem_cacstat (s,BARN_GET_FAIL ); } -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();if (!sheaf)@@ -4961,6 +4989,7 @@ void kmem_cache_return_sheaf(struct kmem {struct slub_percpu_sheaves *pcs;struct node_barn *barn; +int cpu;if (unlikely ((sheaf->capacity != s->sheaf_capacity) || sheaf->pfmemalloc)) {@@ -4969,7 +4998,9 @@ void kmem_cache_return_sheaf(struct kmemreturn ; } -local_lock (&s->cpu_sheaves->lock); +migrate_disable (); + cpu =smp_processor_id (); +qpw_lock (&s->cpu_sheaves->lock, cpu); pcs =this_cpu_ptr (s->cpu_sheaves); barn =get_barn (s);@@ -4979,7 +5010,8 @@ void kmem_cache_return_sheaf(struct kmemstat (s,SHEAF_RETURN_FAST ); } -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();if (!sheaf)return ;@@ -5507,9 +5539,9 @@ slab_empty:*/ static void __pcs_install_empty_sheaf (struct kmem_cache *s,struct slub_percpu_sheaves *pcs,struct slab_sheaf *empty, -struct node_barn *barn) +struct node_barn *barn,int cpu) { -lockdep_assert_held (this_cpu_ptr (&s->cpu_sheaves->lock)); +qpw_lockdep_assert_held (&s->cpu_sheaves->lock);/* This is what we expect to find if nobody interrupted us. */ if (likely (!pcs->spare)) {
mm/slub.c static void __pcs_install_empty_sheaf(st · line 5581
Quoted 8 lines
mm/slub.c +1
-4@@ -5546,31 +5578,34 @@ static void __pcs_install_empty_sheaf(st/* * Replace the full main sheaf with a (at least partially) empty sheaf. * -* Must be called with the cpu_sheaves local lock locked. If successful, returns -* the pcs pointer and the local lock locked (possibly on a different cpu than -* initially called). If not successful, returns NULL and the local lock -* unlocked. +* Must be called with the cpu_sheaves local lock locked, and migration counter
^~ qpw?
mm/slub.c static void __pcs_install_empty_sheaf(st · line 5584
Quoted 3 lines
mm/slub.c +3
-0+* increased. If successful, returns the pcs pointer and the local lock locked +* (possibly on a different cpu than initially called), with migration counter +* increased. If not successful, returns NULL and the local lock unlocked,
^~ qpw?
Quoted 234 lines
mm/slub.c +56
-22+* and migration counter decreased. */ static struct slub_percpu_sheaves *__pcs_replace_full_main (struct kmem_cache *s,struct slub_percpu_sheaves *pcs, -bool allow_spin) +bool allow_spin,int *cpu) {struct slab_sheaf *empty;struct node_barn *barn;bool put_fail; restart: -lockdep_assert_held (this_cpu_ptr (&s->cpu_sheaves->lock)); +qpw_lockdep_assert_held (&s->cpu_sheaves->lock);/* Bootstrap or debug cache, back off */ if (unlikely (!cache_has_sheaves (s))) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();return NULL ; } barn =get_barn (s);if (!barn) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();return NULL ; }@@ -5607,7 +5642,8 @@ restart:stat (s,BARN_PUT_FAIL ); pcs->spare =NULL ; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();sheaf_flush_unused (s, to_flush); empty = to_flush;@@ -5623,7 +5659,8 @@ restart: put_fail = true; alloc_empty: -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, *cpu); +migrate_enable ();/* * alloc_empty_sheaf() doesn't support !allow_spin and it's @@ -5640,11 +5677,17 @@ alloc_empty:if (put_fail)stat (s,BARN_PUT_FAIL ); -if (!sheaf_flush_main (s)) +migrate_disable (); + *cpu =smp_processor_id (); +if (!sheaf_flush_main (s, *cpu)) { +migrate_enable ();return NULL ; + } -if (!local_trylock (&s->cpu_sheaves->lock)) +if (!qpw_trylock (&s->cpu_sheaves->lock, *cpu)) { +migrate_enable ();return NULL ; + } pcs =this_cpu_ptr (s->cpu_sheaves);@@ -5659,13 +5702,14 @@ alloc_empty:return pcs; got_empty: -if (!local_trylock (&s->cpu_sheaves->lock)) { +if (!qpw_trylock (&s->cpu_sheaves->lock, *cpu)) { +migrate_enable ();barn_put_empty_sheaf (barn, empty);return NULL ; } pcs =this_cpu_ptr (s->cpu_sheaves); -__pcs_install_empty_sheaf (s, pcs, empty, barn); +__pcs_install_empty_sheaf (s, pcs, empty, barn, *cpu);return pcs; }@@ -5678,22 +5722,28 @@ static __fastpath_inlinebool free_to_pcs (struct kmem_cache *s,void *object,bool allow_spin) {struct slub_percpu_sheaves *pcs; +int cpu; -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) { +migrate_enable ();return false; + } pcs =this_cpu_ptr (s->cpu_sheaves);if (unlikely (pcs->main->size == s->sheaf_capacity)) { - pcs =__pcs_replace_full_main (s, pcs, allow_spin); + pcs =__pcs_replace_full_main (s, pcs, allow_spin, &cpu);if (unlikely (!pcs))return false; } pcs->main->objects[pcs->main->size++] = object; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat (s,FREE_FASTPATH );@@ -5777,14 +5827,19 @@ bool __kfree_rcu_sheaf(struct kmem_cache {struct slub_percpu_sheaves *pcs;struct slab_sheaf *rcu_sheaf; +int cpu;if (WARN_ON_ONCE (IS_ENABLED (CONFIG_PREEMPT_RT )))return false;lock_map_acquire_try (&kfree_rcu_sheaf_map); -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) { +migrate_enable (); goto fail; + } pcs =this_cpu_ptr (s->cpu_sheaves);@@ -5795,7 +5850,8 @@ bool __kfree_rcu_sheaf(struct kmem_cache/* Bootstrap or debug cache, fall back */ if (unlikely (!cache_has_sheaves (s))) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable (); goto fail; }@@ -5807,7 +5863,8 @@ bool __kfree_rcu_sheaf(struct kmem_cache barn =get_barn (s);if (!barn) { -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable (); goto fail; }@@ -5818,15 +5875,18 @@ bool __kfree_rcu_sheaf(struct kmem_cache goto do_free; } -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable (); empty =alloc_empty_sheaf (s,GFP_NOWAIT );if (!empty) goto fail; -if (!local_trylock (&s->cpu_sheaves->lock)) { +migrate_disable (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) {barn_put_empty_sheaf (barn, empty); +migrate_enable (); goto fail; }@@ -5862,7 +5922,8 @@ do_free:if (rcu_sheaf)call_rcu (&rcu_sheaf->rcu_head, rcu_free_sheaf); -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat (s,FREE_RCU_SHEAF );lock_map_release (&kfree_rcu_sheaf_map);@@ -5889,6 +5950,7 @@ static void free_to_pcs_bulk(struct kmemvoid *remote_objects[PCS_BATCH_MAX ];unsigned int remote_nr =0 ;int node =numa_mem_id (); +int cpu; next_remote_batch:while (i < size) {@@ -5918,7 +5980,9 @@ next_remote_batch: goto flush_remote; next_batch: -if (!local_trylock (&s->cpu_sheaves->lock)) +migrate_disable (); + cpu =smp_processor_id (); +if (!qpw_trylock (&s->cpu_sheaves->lock, cpu)) goto fallback; pcs =this_cpu_ptr (s->cpu_sheaves);@@ -5961,7 +6025,8 @@ do_free:memcpy (main->objects + main->size, p, batch *sizeof (void *)); main->size += batch; -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();stat_add (s,FREE_FASTPATH , batch);@@ -5977,7 +6042,8 @@ do_free:return ; no_empty: -local_unlock (&s->cpu_sheaves->lock); +qpw_unlock (&s->cpu_sheaves->lock, cpu); +migrate_enable ();/* * if we depleted all empty sheaves in the barn or there are too
mm/slub.c static int init_percpu_sheaves(struct km · line 7449
Quoted 8 lines
mm/slub.c +1
-1@@ -7377,7 +7443,7 @@ static int init_percpu_sheaves(struct km pcs =per_cpu_ptr (s->cpu_sheaves, cpu); -local_trylock_init (&pcs->lock); +qpw_trylock_init (&pcs->lock);/* * Bootstrap sheaf has zero size so fast-path allocation fails.
Conversions look correct.
I have some ideas, but I am still not sure about the need of migrate_*able() here, but if they are indeed needed, I think we should work on having them inside helpers that are special for local_cpu-only functions, instead of happening on user code like this.
What do you think?
Thanks for getting this upstream!
Leo
Quoted 19 lines · Marcelo Tosatti
The problem:
Some places in the kernel implement a parallel programming strategy consisting onlocal_locks()for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.On the other hand, for RT workloads this can represent a problem: getting an important workload scheduled out to deal with remote requests is sure to introduce unexpected deadline misses.
The idea:
Currently with PREEMPT_RT=y,local_locks()become per-cpu spinlocks. In this case, instead of scheduling work on a remote cpu, it should be safe to grab that remote cpu's per-cpu spinlock and run the required work locally. That major cost, which is un/locking in every local function, already happens inPREEMPT_RT.
Quoted 6 lines
Also, there is no need to worry about extra cache bouncing:
The cacheline invalidation already happens due toschedule_work_on().This will avoid
schedule_work_on(), and thus avoid scheduling-out an RT workload.
Marcelo, thanks for finishing this series!
Quoted 5 lines
Proposed solution:
A new interface called Queue PerCPU Work (QPW), which should replace Work Queue in the above mentioned use case.If PREEMPT_RT=n this interfaces just wraps the current
Are we enabling it by default in PREEMPT_RT=y? If not,
If CONFIG_QPW=n or qpw=0 this interfaces just wraps the current
Quoted 3 lines
local_locks+ WorkQueue behavior, so no expected change in runtime.If PREEMPT_RT=y, or
CONFIG_QPW=y,queue_percpu_work_on(cpu,...)will
Same here
If CONFIG_QPW=y and qpw=1, queue_percpu_work_on(cpu,...) will
Quoted 8 lines
lock that cpu's per-cpu structure and perform work on it locally. This is possible because on functions that can be used for performing remote work on remote per-cpu structures, the
local_lock(which is already athis_cpuspinlock()), will be replaced by aqpw_spinlock(), which is able to get theper_cpuspinlock()for the cpu passed as parameter.
RFC->v1:
Quoted 9 lines
- Introduce
CONFIG_QPWand qpw= kernel boot option to enable remote spinlocking and execution even on !CONFIG_PREEMPT_RT kernels (Leonardo Bras).
- Movebuffer_headdraining to separate workqueue (Marcelo Tosatti).
- Convert mlock per-CPU page lists to QPW (Marcelo Tosatti).
- Drop memcontrol convertion (as isolated CPUs are not targets ofqueue_work_onanymore).
- Rebase SLUB against Vlastimil's slab/next.
- Add basic document for QPW (Waiman Long).
A document was a nice touch :)
Quoted 105 lines
The following testcase triggers
lru_add_drain_allon an isolated CPU (that doessys_writeto a file before entering its realtime loop)./*
* Simulates a low latency loop program that is interrupted
* due tolru_add_drain_all. To triggerlru_add_drain_all, run:
*
* blockdev --flushbufs/dev/sdX
*
*/
#define_GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdarg.h>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>int cpu;
static void *run(void *arg)
{ pthread_t current_thread; cpu_set_t cpuset; int ret, nrloops; struct sched_param sched_p; pid_t pid; int fd; char buf[] = "xxxxxxxxxxx"; CPU_ZERO(&cpuset); CPU_SET(cpu, &cpuset); current_thread = pthread_self(); ret = pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset); if (ret) { perror("pthread_setaffinity_np failed\n"); exit(0); } memset(&sched_p, 0, sizeof(struct sched_param)); sched_p.sched_priority = 1; pid = gettid(); ret = sched_setscheduler(pid, SCHED_FIFO, &sched_p); if (ret) { perror("sched_setscheduler"); exit(0); } fd = open("/tmp/tmpfile", O_RDWR|O_CREAT|O_TRUNC); if (fd == -1) { perror("open"); exit(0); } ret = write(fd, buf, sizeof(buf)); if (ret == -1) { perror("write"); exit(0); } do { nrloops = nrloops+2; nrloops--; } while (1); }int
main(int argc, char *argv[]){ int fd, ret; pthread_t thread; long val; char *endptr, *str; struct sched_param sched_p; pid_t pid; if (argc != 2) { printf("usage: %s cpu-nr\n", argv[0]); printf("where CPU number is the CPU to pin thread to\n"); exit(0); } str = argv[1]; cpu = strtol(str, &endptr, 10); if (cpu < 0) { printf("strtol returns %d\n", cpu); exit(0); } printf("cpunr=%d\n", cpu); memset(&sched_p, 0, sizeof(struct sched_param)); sched_p.sched_priority = 1; pid = getpid(); ret = sched_setscheduler(pid, SCHED_FIFO, &sched_p); if (ret) { perror("sched_setscheduler"); exit(0);
Quoted 10 lines
} pthread_create(&thread, NULL, run, NULL); sleep(5000); pthread_join(thread, NULL); }
Also, having the reproducer in the cover letter was a great idea!
Thanks!
Leo
Michal Hocko
reviewerQuoted 32 lines · Marcelo Tosatti
The problem:
Some places in the kernel implement a parallel programming strategy consisting onlocal_locks()for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.On the other hand, for RT workloads this can represent a problem: getting an important workload scheduled out to deal with remote requests is sure to introduce unexpected deadline misses.
The idea:
Currently with PREEMPT_RT=y,local_locks()become per-cpu spinlocks. In this case, instead of scheduling work on a remote cpu, it should be safe to grab that remote cpu's per-cpu spinlock and run the required work locally. That major cost, which is un/locking in every local function, already happens inPREEMPT_RT.Also, there is no need to worry about extra cache bouncing:
The cacheline invalidation already happens due toschedule_work_on().This will avoid
schedule_work_on(), and thus avoid scheduling-out an RT workload.Proposed solution:
A new interface called Queue PerCPU Work (QPW), which should replace Work Queue in the above mentioned use case.If PREEMPT_RT=n this interfaces just wraps the current
local_locks+ WorkQueue behavior, so no expected change in runtime.
Quoted 6 lines
If PREEMPT_RT=y, or
CONFIG_QPW=y,queue_percpu_work_on(cpu,...)will lock that cpu's per-cpu structure and perform work on it locally. This is possible because on functions that can be used for performing remote work on remote per-cpu structures, thelocal_lock(which is already athis_cpuspinlock()), will be replaced by aqpw_spinlock(), which is able to get theper_cpuspinlock()for the cpu passed as parameter.
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 40 lines · Michal Hocko
Quoted 32 lines · Marcelo Tosatti
The problem:
Some places in the kernel implement a parallel programming strategy consisting onlocal_locks()for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.On the other hand, for RT workloads this can represent a problem: getting an important workload scheduled out to deal with remote requests is sure to introduce unexpected deadline misses.
The idea:
Currently with PREEMPT_RT=y,local_locks()become per-cpu spinlocks. In this case, instead of scheduling work on a remote cpu, it should be safe to grab that remote cpu's per-cpu spinlock and run the required work locally. That major cost, which is un/locking in every local function, already happens inPREEMPT_RT.Also, there is no need to worry about extra cache bouncing:
The cacheline invalidation already happens due toschedule_work_on().This will avoid
schedule_work_on(), and thus avoid scheduling-out an RT workload.Proposed solution:
A new interface called Queue PerCPU Work (QPW), which should replace Work Queue in the above mentioned use case.If PREEMPT_RT=n this interfaces just wraps the current
local_locks+ WorkQueue behavior, so no expected change in runtime.Quoted 6 lines
If PREEMPT_RT=y, or
CONFIG_QPW=y,queue_percpu_work_on(cpu,...)will lock that cpu's per-cpu structure and perform work on it locally. This is possible because on functions that can be used for performing remote work on remote per-cpu structures, thelocal_lock(which is already athis_cpuspinlock()), will be replaced by aqpw_spinlock(), which is able to get theper_cpuspinlock()for the cpu passed as parameter.
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Michal,
For !PREEMPT_RT, _if_ you select CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken on local_lock, similar to PREEMPT_RT).
If CONFIG_QPW=n, or kernel boot option qpw=n, then only local_lock (and remote work via work_queue) is used.
What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Thanks
Quoted 49 lines · Marcelo Tosatti
Quoted 40 lines · Michal Hocko
Quoted 32 lines · Marcelo Tosatti
The problem:
Some places in the kernel implement a parallel programming strategy consisting onlocal_locks()for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.On the other hand, for RT workloads this can represent a problem: getting an important workload scheduled out to deal with remote requests is sure to introduce unexpected deadline misses.
The idea:
Currently with PREEMPT_RT=y,local_locks()become per-cpu spinlocks. In this case, instead of scheduling work on a remote cpu, it should be safe to grab that remote cpu's per-cpu spinlock and run the required work locally. That major cost, which is un/locking in every local function, already happens inPREEMPT_RT.Also, there is no need to worry about extra cache bouncing:
The cacheline invalidation already happens due toschedule_work_on().This will avoid
schedule_work_on(), and thus avoid scheduling-out an RT workload.Proposed solution:
A new interface called Queue PerCPU Work (QPW), which should replace Work Queue in the above mentioned use case.If PREEMPT_RT=n this interfaces just wraps the current
local_locks+ WorkQueue behavior, so no expected change in runtime.Quoted 6 lines
If PREEMPT_RT=y, or
CONFIG_QPW=y,queue_percpu_work_on(cpu,...)will lock that cpu's per-cpu structure and perform work on it locally. This is possible because on functions that can be used for performing remote work on remote per-cpu structures, thelocal_lock(which is already athis_cpuspinlock()), will be replaced by aqpw_spinlock(), which is able to get theper_cpuspinlock()for the cpu passed as parameter.Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 8 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.
OK, this is not true. There is only CONFIG_QPW and the qpw=yes/no kernel boot option for control.
CONFIG_PREEMPT_RT should probably select CONFIG_QPW=y and CONFIG_QPW_DEFAULT=y.
Quoted 7 lines
What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Thanks
Quoted 56 lines · Marcelo Tosatti
Quoted 55 lines · Marcelo Tosatti
Quoted 40 lines · Michal Hocko
Quoted 32 lines · Marcelo Tosatti
The problem:
Some places in the kernel implement a parallel programming strategy consisting onlocal_locks()for most of the work, and some rare remote operations are scheduled on target cpu. This keeps cache bouncing low since cacheline tends to be mostly local, and avoids the cost of locks in non-RT kernels, even though the very few remote operations will be expensive due to scheduling overhead.On the other hand, for RT workloads this can represent a problem: getting an important workload scheduled out to deal with remote requests is sure to introduce unexpected deadline misses.
The idea:
Currently with PREEMPT_RT=y,local_locks()become per-cpu spinlocks. In this case, instead of scheduling work on a remote cpu, it should be safe to grab that remote cpu's per-cpu spinlock and run the required work locally. That major cost, which is un/locking in every local function, already happens inPREEMPT_RT.Also, there is no need to worry about extra cache bouncing:
The cacheline invalidation already happens due toschedule_work_on().This will avoid
schedule_work_on(), and thus avoid scheduling-out an RT workload.Proposed solution:
A new interface called Queue PerCPU Work (QPW), which should replace Work Queue in the above mentioned use case.If PREEMPT_RT=n this interfaces just wraps the current
local_locks+ WorkQueue behavior, so no expected change in runtime.Quoted 6 lines
If PREEMPT_RT=y, or
CONFIG_QPW=y,queue_percpu_work_on(cpu,...)will lock that cpu's per-cpu structure and perform work on it locally. This is possible because on functions that can be used for performing remote work on remote per-cpu structures, thelocal_lock(which is already athis_cpuspinlock()), will be replaced by aqpw_spinlock(), which is able to get theper_cpuspinlock()for the cpu passed as parameter.Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).
Quoted 8 lines
Quoted 2 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.OK, this is not true. There is only
CONFIG_QPWand the qpw=yes/no kernel boot option for control.
CONFIG_PREEMPT_RTshould probably selectCONFIG_QPW=yandCONFIG_QPW_DEFAULT=y.
Fully agree :)
Quoted 9 lines
Quoted 8 lines
What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Thanks
Michal Hocko
reviewerQuoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).
My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of preempt_disbale -> spinlock on hot paths like SLUB sheeves?
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 11 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 7 lines
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
My bad, actually. Its only CONFIG_QPW on the current patchset.
Quoted 12 lines
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?
Nope, i have not. What is/are the standard benchmarks for SLUB/SLAB allocation ?
Quoted 38 lines · Marcelo Tosatti
Quoted 11 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 7 lines
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
My bad, actually. Its only
CONFIG_QPWon the current patchset.Quoted 16 lines
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Quoted 5 lines
Quoted 2 lines
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Nope, i have not. What is/are the standard benchmarks for SLUB/SLAB allocation ?
Those mentioned here, and I would say also netperf.
https://lore.kernel.org/all/[email protected]/
Michal Hocko
reviewerQuoted 14 lines · Marcelo Tosatti
Quoted 13 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 10 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Michal,
Quoted 7 lines
Quoted 5 lines
Quoted 3 lines
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
My bad, actually. Its only
CONFIG_QPWon the current patchset.
Yeah. PREEMPT_RT -> CONFIG_QPW=y and cmd line makes no difference
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?
Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/
Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT local_locks already get per-cpu spinlocks, so there is only difference for !RT, which as you mention, does preemtp_disable():
The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
What do you think?
Thanks!
Leo
Michal Hocko
reviewerQuoted 57 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Quoted 6 lines
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big. To me, a much more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 11 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big. To me, a much more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.
Signature · 2 lines
Michal Hocko SUSE Labs
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a user->kernel transition").
The IPIs in the patchset above can be deferred until user->kernel transition because they are TLB flushes, for addresses which do not exist on the address space mapping in userspace.
What are the per-CPU objects in SLUB ?
struct slab_sheaf {
union {
struct rcu_head rcu_head;
struct list_head barn_list;
/* only used for prefilled sheafs */
struct {
unsigned int capacity;
bool pfmemalloc;
};
};
struct kmem_cache *cache;
unsigned int size;
int node; /* only used for rcu_sheaf */
void *objects[];
};struct slub_percpu_sheaves {
local_trylock_t lock;
struct slab_sheaf *main; /* never NULL when unlocked */ struct slab_sheaf *spare; /* empty or full, may be NULL */ struct slab_sheaf *rcu_free; /* for batching kfree_rcu() */
};
Examples of local CPU operation that manipulates the data structures:
1) kmalloc, allocates an object from local per CPU list.
2) kfree, returns an object to local per CPU list.
Examples of an operation that would perform changes on the per-CPU lists remotely:kmem_cache_shrink (cache shutdown), kmem_cache_shrink.
You can't delay either kmalloc (removal of object from per-CPU freelist), or kfree (return of object from per-CPU freelist), or kmem_cache_shrink or kmem_cache_shrink to return to userspace.
What i missing something here? (or do you have something on your mind which i can't see).
Michal Hocko
reviewerQuoted 5 lines · Marcelo Tosatti
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").
Nope, I am not talking about IPIs, although those are an example of pcp state as well. I am sorry I do not have a link handy, I am pretty sure Frederic will have that. Another example, though, was vmstat flushes that need to be pcp. There are many other examples.
[...]
Quoted 3 lines
You can't delay either kmalloc (removal of object from per-CPU freelist), or kfree (return of object from per-CPU freelist), or
kmem_cache_shrinkorkmem_cache_shrinkto return to userspace.
Why?
Quoted 2 lines
What i missing something here? (or do you have something on your mind which i can't see).
I am really sorry for being really vague here. Let me try to draw a more abstract problem definition and let's see whether we are trying to solve the same problem here. Maybe not...
I believe the main usecase of the interest here is uninterrupted userspace execution and delayed pcp work that migh disturb such workload after it has returned to the userspace. Right?
That is usually hauskeeping work that for, performance reasons, doesn't happen in hot paths while the workload was executing in the kernel space.
There are more ways to deal with that. You can either change the hot path to not require deferred operation (tricky withtout introducing regressions for most workloads) or you can define a more suitable place to perform the housekeeping while still running in the kernel.
Your QWP work relies on local_lock -> spin_lock transition and performing the pcp work remotely so you do not need to disturb that remote cpu. Correct?
Alternative approach is to define a moment when the housekeeping operation is performed on that local cpu while still running in the kernel space - e.g. when returning to the userspace. Delayed work is then not necessary and userspace is not disrupted after returning to the userspace.
Do I make more sense or does the above sound like a complete gibberish?
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 12 lines · Michal Hocko
Quoted 5 lines · Marcelo Tosatti
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").Nope, I am not talking about IPIs, although those are an example of pcp state as well. I am sorry I do not have a link handy, I am pretty sure Frederic will have that. Another example, though, was vmstat flushes that need to be pcp. There are many other examples.
Quoted 7 lines
[...]
Quoted 3 lines
You can't delay either kmalloc (removal of object from per-CPU freelist), or kfree (return of object from per-CPU freelist), or
kmem_cache_shrinkorkmem_cache_shrinkto return to userspace.Why?
Because kernel code might need to use that object right away, so it needs to be allocated right after kmalloc returns.
Quoted 9 lines
Quoted 2 lines
What i missing something here? (or do you have something on your mind which i can't see).
I am really sorry for being really vague here. Let me try to draw a more abstract problem definition and let's see whether we are trying to solve the same problem here. Maybe not...
I believe the main usecase of the interest here is uninterrupted userspace execution
The main usecase of interest is uninterrupted userspace execution, yes.
It is a good thing if you can enter the kernel, say perform system calls, and not be interrupted as well.
Quoted 15 lines
and delayed pcp work that migh disturb such workload
after it has returned to the userspace. Right?
That is usually hauskeeping work that for, performance reasons, doesn't happen in hot paths while the workload was executing in the kernel space.There are more ways to deal with that. You can either change the hot path to not require deferred operation (tricky withtout introducing regressions for most workloads) or you can define a more suitable place to perform the housekeeping while still running in the kernel.
Your QWP work relies on
local_lock->spin_locktransition and performing the pcp work remotely so you do not need to disturb that remote cpu. Correct?
Quoted 7 lines
Alternative approach is to define a moment when the housekeeping operation is performed on that local cpu while still running in the kernel space - e.g. when returning to the userspace. Delayed work is then not necessary and userspace is not disrupted after returning to the userspace.
Do I make more sense or does the above sound like a complete gibberish?
OK, sure, but can't see how you can do that with per-CPU caches for kmalloc, for example.
Michal Hocko
reviewerQuoted 2 lines · Marcelo Tosatti
Quoted 1 line · Michal Hocko
On Thu 19-02-26 12:27:23, Marcelo Tosatti wrote:
[...]
Quoted 21 lines
Quoted 21 lines
and delayed pcp work that migh disturb such workload
after it has returned to the userspace. Right?
That is usually hauskeeping work that for, performance reasons, doesn't happen in hot paths while the workload was executing in the kernel space.There are more ways to deal with that. You can either change the hot path to not require deferred operation (tricky withtout introducing regressions for most workloads) or you can define a more suitable place to perform the housekeeping while still running in the kernel.
Your QWP work relies on
local_lock->spin_locktransition and performing the pcp work remotely so you do not need to disturb that remote cpu. Correct?Alternative approach is to define a moment when the housekeeping operation is performed on that local cpu while still running in the kernel space - e.g. when returning to the userspace. Delayed work is then not necessary and userspace is not disrupted after returning to the userspace.
Quoted 4 lines
Quoted 1 line
Do I make more sense or does the above sound like a complete gibberish?
OK, sure, but can't see how you can do that with per-CPU caches for kmalloc, for example.
As we have discussed in other subthread. By flushing those pcp caches on the return to userspace. Those flushes are not needed immediately. They just need to happen to allow operations listed by Vlastimil to finish. Or to avoid the problem by not using them but that is a separate discussion.
I believe we can establish that any pcp delayed operation implemented through WQs can be flushed on the way to the userspace, right? The performance might be suboptimal but correctness will be preserved. So doing this on isolated CPUs could be an alternative to making changes to the pcp WQ handling.
I haven't checked the WQ code deeply but I believe it should be feasible to flush all pcp WQs with pending work on the isolated cpu when the isolated workload returns to the userspace. This way we wouldn't need to special case each and every one of them.
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 42 lines · Michal Hocko
Quoted 2 lines · Marcelo Tosatti
Quoted 1 line · Michal Hocko
On Thu 19-02-26 12:27:23, Marcelo Tosatti wrote:
[...]
Quoted 21 lines
Quoted 21 lines
and delayed pcp work that migh disturb such workload
after it has returned to the userspace. Right?
That is usually hauskeeping work that for, performance reasons, doesn't happen in hot paths while the workload was executing in the kernel space.There are more ways to deal with that. You can either change the hot path to not require deferred operation (tricky withtout introducing regressions for most workloads) or you can define a more suitable place to perform the housekeeping while still running in the kernel.
Your QWP work relies on
local_lock->spin_locktransition and performing the pcp work remotely so you do not need to disturb that remote cpu. Correct?Alternative approach is to define a moment when the housekeeping operation is performed on that local cpu while still running in the kernel space - e.g. when returning to the userspace. Delayed work is then not necessary and userspace is not disrupted after returning to the userspace.
Quoted 4 lines
Quoted 1 line
Do I make more sense or does the above sound like a complete gibberish?
OK, sure, but can't see how you can do that with per-CPU caches for kmalloc, for example.
As we have discussed in other subthread. By flushing those pcp caches on the return to userspace. Those flushes are not needed immediately. They just need to happen to allow operations listed by Vlastimil to finish. Or to avoid the problem by not using them but that is a separate discussion.
I believe we can establish that any pcp delayed operation implemented through WQs can be flushed on the way to the userspace, right? The performance might be suboptimal but correctness will be preserved. So doing this on isolated CPUs could be an alternative to making changes to the pcp WQ handling.
Quoted 4 lines
I haven't checked the WQ code deeply but I believe it should be feasible to flush all pcp WQs with pending work on the isolated cpu when the isolated workload returns to the userspace. This way we wouldn't need to special case each and every one of them.
If you look at flush_scheduled_work(), there is a big compile time warning to prevent from using it because it's too deadlock prone.
But even if we flushed only some specific relevant workqueues, I'm not sure that would help. The problem is not so much about locally queued workqueue handling. On a preempt kernel, that would most likely issue a context switch right away to handle the work. The problem is more about remote queuing while the CPU is in critical code in userspace.
Also such a flush or local queue would involve multitasking and tick restart. We could handle that with forcing to be stopped on userspace entry but that doesn't simplify the picture.
Thanks.
Signature · 2 lines
Frederic Weisbecker SUSE Labs
Quoted 11 lines · Michal Hocko
Quoted 5 lines · Marcelo Tosatti
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").Nope, I am not talking about IPIs, although those are an example of pcp state as well. I am sorry I do not have a link handy, I am pretty sure Frederic will have that. Another example, though, was vmstat flushes that need to be pcp. There are many other examples.
Here it is:
https://lore.kernel.org/all/[email protected]/
Thanks.
Signature · 2 lines
Frederic Weisbecker SUSE Labs
Quoted 13 lines · Frederic Weisbecker
Quoted 11 lines · Michal Hocko
Quoted 5 lines · Marcelo Tosatti
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").Nope, I am not talking about IPIs, although those are an example of pcp state as well. I am sorry I do not have a link handy, I am pretty sure Frederic will have that. Another example, though, was vmstat flushes that need to be pcp. There are many other examples.
Quoted 5 lines
Here it is:
https://lore.kernel.org/all/[email protected]/
Thanks.
Frederic,
I think this is a valid solution, however on systems with many CPUs, in nohz_full, performing system calls, can't there be significant increase of lru_lock contention ? Consider 100+ CPUs performing many system calls which add 1 or 2 folios to per-CPU LRU lists.
Note: if you are confident about the above not being a problem, this approach looks good to me.
commit eb709b0d062efd653a61183af8e27b2711c3cf5c
Author: Shaohua Li <[email protected]> Date: Tue May 24 17:12:55 2011 -0700
mm: batch activate_page() to reduce lock contention
The zone->lru_lock is heavily contented in workload where activate_page() is frequently used. We could do batch activate_page() to reduce the lock contention. The batched pages will be added into zone list when the pool is full or page reclaim is trying to drain them.
For example, in a 4 socket 64 CPU system, create a sparse file and 64 processes, processes shared map to the file. Each process read access the whole file and then exit. The process exit will do unmap_vmas() and cause a lot of activate_page() call. In such workload, we saw about 58% total time reduction with below patch. Other workloads with a lot of activate_page also benefits a lot too.
...
The most significent are:
case-lru-file-readtwice -11.69%
case-mmap-pread-rand -15.26%
case-mmap-pread-seq -69.72%Some Gemini answers (question was "list of nohz_full usecases"):
2. Scientific Simulation & Research
Research institutions (like CERN, NASA, or national labs) use nohz_full for "tightly coupled" parallel workloads.
Workloads: Molecular dynamics, fluid dynamics (CFD), and weather forecasting (e.g., WRF models).
The "Barrier" Problem: In massive clusters using MPI (Message Passing Interface), all CPUs often have to reach a synchronization barrier before the next step of a simulation. If one CPU is delayed by a few milliseconds due to a timer tick, all other thousands of CPUs sit idle waiting for it. nohz_full prevents this "tail latency" from stalling the entire supercomputer.
...
4. Competitive Benchmarking & Kernel Development
Performance engineers use this mode to get "clean" numbers when testing new hardware or compilers.
Workloads: Core-to-core latency tests, cache-bandwidth benchmarks, and standard suites like SPEC CPU.
Goal: Eliminating the "noise" of the operating system so that the results reflect pure hardware performance.
...
Summary Table: Who uses nohz_full?
User Group Primary Workload Why they use it Quant Firms High-Frequency Trading To prevent micro-stutter during trade execution. Research Labs MPI-based Simulations To avoid the "slowest node" stalling the whole cluster. Telcos/ISP 5G/Packet Processing To ensure wire-speed processing without interrupts.
Hardware Vendors Chip Validation To benchmark CPU performance without OS interference.
Here is how scientific simulations handle system calls:
1. The "Compute-Loop" (Low Syscall)
The core of a simulation (like a GROMACS molecular dynamics step) is just raw math: fetching data from RAM, doing floating-point arithmetic (AVX/SSE), and writing it back.
During the loop: The CPU stays in "Userspace" for millions of cycles without ever asking the kernel for help.
Why it works: Since there are no system calls, nohz_full can successfully turn off the timer tick, allowing the CPU to focus 100% on the math.
2. The "Communication-Phase" (High Syscall)
System calls usually happen only at the end of a computation block, when the simulation needs to talk to other nodes.
The Tools: MPI (Message Passing Interface) uses system calls like write, sendmsg, or specialized RDMA calls to move data across the network.
The Pattern: These simulations follow a "Burst" pattern—long periods of zero system calls (computation) followed by a quick burst of system calls (synchronization).
3. When are they "Syscall Intensive"?
There are specific parts of a simulation that are intensive, but researchers try to minimize them:
I/O Operations: Writing "checkpoints" or large trajectory files to disk (using write()). This is why high-end HPC systems use Asynchronous I/O or dedicated I/O nodes—to keep the compute cores from getting bogged down in system calls.
Memory Allocation: Constantly calling malloc/free involves the brk or mmap system calls. Optimized simulation tools pre-allocate all the memory they need at startup to avoid this.
Quoted 18 lines · Marcelo Tosatti
Quoted 17 lines · Frederic Weisbecker
Quoted 11 lines · Michal Hocko
Quoted 5 lines · Marcelo Tosatti
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").Nope, I am not talking about IPIs, although those are an example of pcp state as well. I am sorry I do not have a link handy, I am pretty sure Frederic will have that. Another example, though, was vmstat flushes that need to be pcp. There are many other examples.
Here it is:
Quoted 8 lines
Quoted 1 line
Thanks.
Frederic,
I think this is a valid solution, however on systems with many CPUs, in
nohz_full, performing system calls, can't there be significant increase oflru_lockcontention ? Consider 100+ CPUs performing many system calls which add 1 or 2 folios to per-CPU LRU lists.
That's more a question for Michal or Vlastimil.
Quoted 35 lines
Note: if you are confident about the above not being a problem, this approach looks good to me.
commit
eb709b0d062efd653a61183af8e27b2711c3cf5cAuthor: Shaohua Li <[email protected]> Date: Tue May 24 17:12:55 2011 -0700mm: batch
activate_page()to reduce lock contentionThe
zone->lru_lockis heavily contented in workload whereactivate_page()is frequently used. We could do batchactivate_page()to reduce the lock contention. The batched pages will be added into zone list when the pool is full or page reclaim is trying to drain them.For example, in a 4 socket 64 CPU system, create a sparse file and 64 processes, processes shared map to the file. Each process read access the whole file and then exit. The process exit will do
unmap_vmas()and cause a lot ofactivate_page()call. In such workload, we saw about 58% total time reduction with below patch. Other workloads with a lot ofactivate_pagealso benefits a lot too....
The most significent are: case-lru-file-readtwice -11.69% case-mmap-pread-rand -15.26% case-mmap-pread-seq -69.72%Some Gemini answers (question was "list of
nohz_fullusecases"):2. Scientific Simulation & Research
Research institutions (like CERN, NASA, or national labs) use
nohz_fullfor "tightly coupled" parallel workloads.
Quoted 8 lines
Workloads: Molecular dynamics, fluid dynamics (CFD), and weather forecasting (e.g., WRF models).
The "Barrier" Problem: In massive clusters using MPI (Message Passing Interface), all CPUs often have to reach a synchronization barrier before the next step of a simulation. If one CPU is delayed by a few milliseconds due to a timer tick, all other thousands of CPUs sit idle waiting for it.
nohz_fullprevents this "tail latency" from stalling the entire supercomputer.
Wow! I heard about that possible usecase but I didn't know it was really used in practice. I privately dug into this with gemini and there are actual usecase references.
Quoted 7 lines
...
4. Competitive Benchmarking & Kernel Development
Performance engineers use this mode to get "clean" numbers when testing new hardware or compilers.
Quoted 5 lines
Workloads: Core-to-core latency tests, cache-bandwidth benchmarks, and standard suites like SPEC CPU.
Goal: Eliminating the "noise" of the operating system so that the results reflect pure hardware performance.
Ok didn't know about that one either.
Quoted 40 lines
Summary Table: Who uses
nohz_full?User Group Primary Workload Why they use it Quant Firms High-Frequency Trading To prevent micro-stutter during trade execution. Research Labs MPI-based Simulations To avoid the "slowest node" stalling the whole cluster. Telcos/ISP 5G/Packet Processing To ensure wire-speed processing without interrupts.Hardware Vendors Chip Validation To benchmark CPU performance without OS interference.
Here is how scientific simulations handle system calls:
1. The "Compute-Loop" (Low Syscall)
The core of a simulation (like a GROMACS molecular dynamics step) is just raw math: fetching data from RAM, doing floating-point arithmetic (AVX/SSE), and writing it back.
During the loop: The CPU stays in "Userspace" for millions of cycles without ever asking the kernel for help.
Why it works: Since there are no system calls,
nohz_fullcan successfully turn off the timer tick, allowing the CPU to focus 100% on the math.2. The "Communication-Phase" (High Syscall)
System calls usually happen only at the end of a computation block, when the simulation needs to talk to other nodes.
The Tools: MPI (Message Passing Interface) uses system calls like write, sendmsg, or specialized RDMA calls to move data across the network.
The Pattern: These simulations follow a "Burst" pattern—long periods of zero system calls (computation) followed by a quick burst of system calls (synchronization).
3. When are they "Syscall Intensive"?
There are specific parts of a simulation that are intensive, but researchers try to minimize them:
Quoted 8 lines
I/O Operations: Writing "checkpoints" or large trajectory files to disk (using
write()). This is why high-end HPC systems use Asynchronous I/O or dedicated I/O nodes—to keep the compute cores from getting bogged down in system calls.Memory Allocation: Constantly calling malloc/free involves the brk or mmap system calls. Optimized simulation tools pre-allocate all the memory they need at startup to avoid this.
Ok. I asked a similar question and got this (you made me use an LLM for the first time btw, I held out for 4 years... I'm sure I can wait 4 more years until the next usage :o)
Does MPI synchronization only use userspace code or does it rely on syscalls?
The short answer is: **both**, but the goal of HPC (High Performance Computing) is to use as few **syscalls** (system calls) as possible.
Here is the technical breakdown:
### 1. The "Fast Path" (User-Space only)
To maximize speed, modern MPI implementations (like OpenMPI or MPICH) try to stay in **User-Space** as much as possible.
* **Shared Memory (Intra-node):** If two processes are running on the same server, they communicate via a shared memory zone. Synchronization is handled by **spinlocks** or **software barriers**. The CPU "polls" (loops) on a memory variable until it changes. **No syscall is involved here.**
* **RDMA / InfiniBand (Inter-node):** With high-performance network cards, MPI uses **RDMA** (Remote Direct Memory Access). The user code instructs the network card to write directly into the memory of the distant server. Once configured, data exchange happens without involving the Linux kernel.
### 2. The "Slow Path" (System Calls / Syscalls)
Passing through the kernel (a syscall) is necessary in certain situations, but it is "expensive" because it forces a **context switch**, which flushes CPU caches.
* **Initialization:** During startup (MPI_Init), many syscalls are used to create sockets, map shared memory (mmap), and configure network interfaces.
* **Standard TCP/IP:** If you are not using a high-performance network (RDMA) but simple Ethernet instead, MPI must call send() and recv(), which are syscalls. The Linux kernel then takes over to manage the TCP/IP stack.
* **Sleep Mode (Blocking):** If an MPI process waits for a message for too long, it may decide to "go to sleep" to yield the CPU to another task via syscalls like futex() or poll().
**In summary:** MPI synchronization aims to be **100% User-Space** (via memory polling) to avoid syscall latency. It is precisely because MPI tries to bypass the kernel that we use nohz_full: we are asking the kernel not to even "knock on the CPU's door" with its clock interruptions.
Michal Hocko
reviewerQuoted 21 lines · Frederic Weisbecker
Quoted 20 lines · Marcelo Tosatti
Quoted 18 lines · Frederic Weisbecker
Quoted 11 lines · Michal Hocko
Quoted 5 lines · Marcelo Tosatti
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").Nope, I am not talking about IPIs, although those are an example of pcp state as well. I am sorry I do not have a link handy, I am pretty sure Frederic will have that. Another example, though, was vmstat flushes that need to be pcp. There are many other examples.
Here it is:
https://lore.kernel.org/all/[email protected]/
Thanks.
Quoted 8 lines
Quoted 6 lines
Frederic,
I think this is a valid solution, however on systems with many CPUs, in
nohz_full, performing system calls, can't there be significant increase oflru_lockcontention ? Consider 100+ CPUs performing many system calls which add 1 or 2 folios to per-CPU LRU lists.That's more a question for Michal or Vlastimil.
And practically speaking we would need to measure that on real workloads to be sure. Keep in mind there is still batching going on. We just flush the remaining of it on the way back to the userspace.
Signature · 2 lines
Michal Hocko SUSE Labs
On Wed, Feb 25, 2026 at 10:49:54PM +0100, Frederic Weisbecker wrote:
<snip>
Quoted 8 lines
Quoted 8 lines
There are specific parts of a simulation that are intensive, but researchers try to minimize them:
I/O Operations: Writing "checkpoints" or large trajectory files to disk (using
write()). This is why high-end HPC systems use Asynchronous I/O or dedicated I/O nodes—to keep the compute cores from getting bogged down in system calls.
Quoted 7 lines
Quoted 3 lines
Memory Allocation: Constantly calling malloc/free involves the brk or mmap system calls. Optimized simulation tools pre-allocate all the memory they need at startup to avoid this.
Ok. I asked a similar question and got this (you made me use an LLM for the first time btw, I held out for 4 years... I'm sure I can wait 4 more years until the next usage :o)
You should use it more often, it can save a significant amount of time :-)
Quoted 9 lines
### 2. The "Slow Path" (System Calls / Syscalls)
Passing through the kernel (a syscall) is necessary in certain situations, but it is "expensive" because it forces a **context switch**, which flushes CPU caches.
* **Initialization:** During startup (
MPI_Init), many syscalls are used to create sockets, map shared memory (mmap), and configure network interfaces.
* **Standard TCP/IP:** If you are not using a high-performance network (RDMA) but simple Ethernet instead, MPI must callsend()andrecv(), which are syscalls. The Linux kernel then takes over to manage the TCP/IP stack.
* **Sleep Mode (Blocking):** If an MPI process waits for a message for too long, it may decide to "go to sleep" to yield the CPU to another task via syscalls likefutex()orpoll().**In summary:** MPI synchronization aims to be **100% User-Space** (via memory polling) to avoid syscall latency. It is precisely because MPI tries to bypass the kernel that we use
nohz_full: we are asking the kernel not to even "knock on the CPU's door" with its clock interruptions.
Of course, there is a cost to system calls. However, considering "low latency applications must necessarily remain in userspace, therefore lets optimize only for that case" is limiting IMHO.
Should avoid interruptions whenever possible, for isolated CPUs (in userspace _and_ kernelspace).
Quoted 16 lines · Marcelo Tosatti
On Wed, Feb 25, 2026 at 10:49:54PM +0100, Frederic Weisbecker wrote:
<snip>
Quoted 12 lines
Quoted 11 lines
There are specific parts of a simulation that are intensive, but researchers try to minimize them:
I/O Operations: Writing "checkpoints" or large trajectory files to disk (using
write()). This is why high-end HPC systems use Asynchronous I/O or dedicated I/O nodes—to keep the compute cores from getting bogged down in system calls.Memory Allocation: Constantly calling malloc/free involves the brk or mmap system calls. Optimized simulation tools pre-allocate all the memory they need at startup to avoid this.
Quoted 6 lines
Quoted 3 lines
Ok. I asked a similar question and got this (you made me use an LLM for the first time btw, I held out for 4 years... I'm sure I can wait 4 more years until the next usage :o)
You should use it more often, it can save a significant amount of time :-)
I fear the earth doesn't have the resources to serve daily use of LLM to us all. Meanwhile it was a pleasant surprise to see it in action and answer questions I had to myself for a long while. And I might use it again on the rare occasions where a simple search engine request doesn't do the job.
Quoted 8 lines
Quoted 8 lines
### 2. The "Slow Path" (System Calls / Syscalls)
Passing through the kernel (a syscall) is necessary in certain situations, but it is "expensive" because it forces a **context switch**, which flushes CPU caches.
* **Initialization:** During startup (
MPI_Init), many syscalls are used to create sockets, map shared memory (mmap), and configure network interfaces.
* **Standard TCP/IP:** If you are not using a high-performance network (RDMA) but simple Ethernet instead, MPI must callsend()andrecv(), which are syscalls. The Linux kernel then takes over to manage the TCP/IP stack.
* **Sleep Mode (Blocking):** If an MPI process waits for a message for too long, it may decide to "go to sleep" to yield the CPU to another task via syscalls likefutex()orpoll().
Quoted 8 lines
Quoted 1 line
**In summary:** MPI synchronization aims to be **100% User-Space** (via memory polling) to avoid syscall latency. It is precisely because MPI tries to bypass the kernel that we use
nohz_full: we are asking the kernel not to even "knock on the CPU's door" with its clock interruptions.Of course, there is a cost to system calls. However, considering "low latency applications must necessarily remain in userspace, therefore lets optimize only for that case" is limiting IMHO.
Should avoid interruptions whenever possible, for isolated CPUs (in userspace _and_ kernelspace).
Very low latency requirements really should bend toward full userspace. But you're right that isolation (even full with nohz_full) should probably not be limited to that. HPC shows such a usecase where the workload is not perfectly isolated and yet nohz_full brings improvements.
Thanks.
Signature · 2 lines
Frederic Weisbecker SUSE Labs
Quoted 45 lines · Marcelo Tosatti
On Mon, Feb 16, 2026 at 12:00:55PM +0100, Michal Hocko wrote:
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").The IPIs in the patchset above can be deferred until
user->kerneltransition because they are TLB flushes, for addresses which do not exist on the address space mapping in userspace.What are the per-CPU objects in SLUB ?
struct slab_sheaf { union { struct rcu_head rcu_head; struct list_head barn_list; /* only used for prefilled sheafs */ struct { unsigned int capacity; bool pfmemalloc; }; }; struct kmem_cache *cache; unsigned int size; int node; /* only used for rcu_sheaf */ void *objects[]; };struct
slub_percpu_sheaves{
local_trylock_tlock;
structslab_sheaf*main; /* never NULL when unlocked */ structslab_sheaf*spare; /* empty or full, may be NULL */ structslab_sheaf*rcu_free; /* for batchingkfree_rcu()*/
};Examples of local CPU operation that manipulates the data structures:
1) kmalloc, allocates an object from local per CPU list.
2) kfree, returns an object to local per CPU list.Examples of an operation that would perform changes on the per-CPU lists remotely:
kmem_cache_shrink(cache shutdown),kmem_cache_shrink.
Quoted 6 lines
You can't delay either kmalloc (removal of object from per-CPU freelist), or kfree (return of object from per-CPU freelist), or
kmem_cache_shrinkorkmem_cache_shrinkto return to userspace.What i missing something here? (or do you have something on your mind which i can't see).
Let's try and analyze when we need to do the flushing in SLUB
- memory offline - would anyone do that with isolcpus? if yes, they probably deserve the disruption
- cache shrinking (mainly from sysfs handler) - not necessary for correctness, can probably skip cpu if needed, also kinda shooting your own foot on isolcpu systems
- kmem_cache is being destroyed (__kmem_cache_shutdown()) - this is important for correctness. destroying caches should be rare, but can't rule it out
- kvfree_rcu_barrier() - a very tricky one; currently has only a debugging caller, but that can change
(BTW, see the note in flush_rcu_sheaves_on_cache() and how it relies on the flush actually happening on the cpu. Won't QPW violate that?)
How would this work with houskeeping on return to userspace approach?
- Would we just walk the list of all caches to flush them? could be expensive. Would we somehow note only those that need it? That would make the fast paths do something extra?
- If some other CPU executed kmem_cache_destroy(), it would have to wait for the isolated cpu returning to userspace. Do we have the means for synchronizing on that? Would that risk a deadlock? We used to have a deferred finishing of the destroy for other reasons but were glad to get rid of it when it was possible, now it might be necessary to revive it?
How would this work with QPW?
- probably fast paths more expensive due to spin lock vs local_trylock_t
- flush_rcu_sheaves_on_cache() needs to be solved safely (see above)
What if we avoid percpu sheaves completely on isolated cpus and instead allocate/free using the slowpaths?
- It could probably be achieved without affecting fastpaths, as we already handle bootstrap without sheaves, so it's implemented in a way to not affect fastpaths.
- Would it slow the isolcpu workloads down too much when they do a syscall?
- compared to "houskeeping on return to userspace" flushing, maybe not?
Because in that case the syscall starts with sheaves flushed from previous return, it has to do something expensive to get the initial sheaf, then maybe will use only on or few objects, then on return has to flush everything. Likely the slowpath might be faster, unless it allocates/frees many objects from the same cache.
- compared to QPW - it would be slower as QPW would mostly retain sheaves
populated, the need for flushes should be very rare
So if we can assume that workloads on isolated cpus make syscalls only rarely, and when they do they can tolerate them being slower, I think the "avoid sheaves on isolated cpus" would be the best way here.
Michal Hocko
reviewerQuoted 66 lines · Vlastimil Babka
Quoted 45 lines · Marcelo Tosatti
On Mon, Feb 16, 2026 at 12:00:55PM +0100, Michal Hocko wrote:
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").The IPIs in the patchset above can be deferred until
user->kerneltransition because they are TLB flushes, for addresses which do not exist on the address space mapping in userspace.What are the per-CPU objects in SLUB ?
struct slab_sheaf { union { struct rcu_head rcu_head; struct list_head barn_list; /* only used for prefilled sheafs */ struct { unsigned int capacity; bool pfmemalloc; }; }; struct kmem_cache *cache; unsigned int size; int node; /* only used for rcu_sheaf */ void *objects[]; };struct
slub_percpu_sheaves{
local_trylock_tlock;
structslab_sheaf*main; /* never NULL when unlocked */ structslab_sheaf*spare; /* empty or full, may be NULL */ structslab_sheaf*rcu_free; /* for batchingkfree_rcu()*/
};Examples of local CPU operation that manipulates the data structures:
1) kmalloc, allocates an object from local per CPU list.
2) kfree, returns an object to local per CPU list.Examples of an operation that would perform changes on the per-CPU lists remotely:
kmem_cache_shrink(cache shutdown),kmem_cache_shrink.Quoted 6 lines
You can't delay either kmalloc (removal of object from per-CPU freelist), or kfree (return of object from per-CPU freelist), or
kmem_cache_shrinkorkmem_cache_shrinkto return to userspace.What i missing something here? (or do you have something on your mind which i can't see).
Let's try and analyze when we need to do the flushing in SLUB
- memory offline - would anyone do that with isolcpus? if yes, they probably deserve the disruption
- cache shrinking (mainly from sysfs handler) - not necessary for correctness, can probably skip cpu if needed, also kinda shooting your own foot on isolcpu systems
-
kmem_cacheis being destroyed (__kmem_cache_shutdown()) - this is important for correctness. destroying caches should be rare, but can't rule it out
Quoted 5 lines
-
kvfree_rcu_barrier()- a very tricky one; currently has only a debugging caller, but that can change(BTW, see the note in
flush_rcu_sheaves_on_cache()and how it relies on the flush actually happening on the cpu. Won't QPW violate that?)
Thanks, this is a very useful insight.
Quoted 11 lines
How would this work with houskeeping on return to userspace approach?
- Would we just walk the list of all caches to flush them? could be expensive. Would we somehow note only those that need it? That would make the fast paths do something extra?
- If some other CPU executed
kmem_cache_destroy(), it would have to wait for the isolated cpu returning to userspace. Do we have the means for synchronizing on that? Would that risk a deadlock? We used to have a deferred finishing of the destroy for other reasons but were glad to get rid of it when it was possible, now it might be necessary to revive it?
This would be tricky because there is no time guarantee when isolated workload enters the kernel again. Maybe never if all the pre-initialization was sufficient. On the other hand if the flush happens on the way to userspace then you only need to wait for the isolated workload to return from a syscall (modulo task dying and similar edge cases).
Quoted 8 lines
How would this work with QPW?
- probably fast paths more expensive due to spin lock vs
local_trylock_t-
flush_rcu_sheaves_on_cache()needs to be solved safely (see above)What if we avoid percpu sheaves completely on isolated cpus and instead allocate/free using the slowpaths?
That seems like a reasonable performance price to pay for very edge case (isolated workload).
Signature · 2 lines
Michal Hocko SUSE Labs
Hi Vlastimil,
Quoted 50 lines · Vlastimil Babka
Quoted 49 lines · Marcelo Tosatti
On Mon, Feb 16, 2026 at 12:00:55PM +0100, Michal Hocko wrote:
Michal,
Again, i don't see how moving operations to happen at return to kernel would help (assuming you are talking about "context_tracking,x86: Defer some IPIs until a
user->kerneltransition").The IPIs in the patchset above can be deferred until
user->kerneltransition because they are TLB flushes, for addresses which do not exist on the address space mapping in userspace.What are the per-CPU objects in SLUB ?
struct slab_sheaf { union { struct rcu_head rcu_head; struct list_head barn_list; /* only used for prefilled sheafs */ struct { unsigned int capacity; bool pfmemalloc; }; }; struct kmem_cache *cache; unsigned int size; int node; /* only used for rcu_sheaf */ void *objects[]; };struct
slub_percpu_sheaves{
local_trylock_tlock;
structslab_sheaf*main; /* never NULL when unlocked */ structslab_sheaf*spare; /* empty or full, may be NULL */ structslab_sheaf*rcu_free; /* for batchingkfree_rcu()*/
};Examples of local CPU operation that manipulates the data structures:
1) kmalloc, allocates an object from local per CPU list.
2) kfree, returns an object to local per CPU list.Examples of an operation that would perform changes on the per-CPU lists remotely:
kmem_cache_shrink(cache shutdown),kmem_cache_shrink.You can't delay either kmalloc (removal of object from per-CPU freelist), or kfree (return of object from per-CPU freelist), or
kmem_cache_shrinkorkmem_cache_shrinkto return to userspace.
Quoted 7 lines
Quoted 2 lines
What i missing something here? (or do you have something on your mind which i can't see).
Let's try and analyze when we need to do the flushing in SLUB
- memory offline - would anyone do that with isolcpus? if yes, they probably deserve the disruption
I think its OK to avoid memory offline on such systems.
Quoted 8 lines
- cache shrinking (mainly from sysfs handler) - not necessary for correctness, can probably skip cpu if needed, also kinda shooting your own foot on isolcpu systems
-
kmem_cacheis being destroyed (__kmem_cache_shutdown()) - this is important for correctness. destroying caches should be rare, but can't rule it out
Quoted 5 lines
-
kvfree_rcu_barrier()- a very tricky one; currently has only a debugging caller, but that can change(BTW, see the note in
flush_rcu_sheaves_on_cache()and how it relies on the flush actually happening on the cpu. Won't QPW violate that?)
(struct kmem_cache *s)->cpu_sheaves (percpu)->rcu_free with the s->cpu_sheaves->lock lock held:
do_free:
rcu_sheaf = pcs->rcu_free;
/*
* Since we flush immediately when size reaches capacity, we never reach
* this with size already at capacity, so no OOB write is possible.
*/
rcu_sheaf->objects[rcu_sheaf->size++] = obj;
if (likely(rcu_sheaf->size < s->sheaf_capacity)) {
rcu_sheaf = NULL;
} else {
pcs->rcu_free = NULL;
rcu_sheaf->node = numa_mem_id();
}
/*
* we flush before local_unlock to make sure a racing
* flush_all_rcu_sheaves() doesn't miss this sheaf
*/
if (rcu_sheaf)
call_rcu(&rcu_sheaf->rcu_head, rcu_free_sheaf);
qpw_unlock(&s->cpu_sheaves->lock, cpu);So if it invokes call_rcu, it sets pcs->rcu_free = NULL. In that case, for flush_rcu_sheaf executing remotely from flush_rcu_sheaves_on_cache will:
static void flush_rcu_sheaf(struct work_struct *w)
{
struct slub_percpu_sheaves *pcs;
struct slab_sheaf *rcu_free;
struct slub_flush_work *sfw;
struct kmem_cache *s;
int cpu = qpw_get_cpu(w);
sfw = &per_cpu(slub_flush, cpu);
s = sfw->s;
qpw_lock(&s->cpu_sheaves->lock, cpu);
pcs = per_cpu_ptr(s->cpu_sheaves, cpu);
rcu_free = pcs->rcu_free;
pcs->rcu_free = NULL;
qpw_unlock(&s->cpu_sheaves->lock, cpu);
if (rcu_free)
call_rcu(&rcu_free->rcu_head, rcu_free_sheaf_nobarn);
}Only call rcu_free_sheaf_nobarn if pcs->rcu_free is not NULL.
So it seems safe?
Quoted 11 lines
How would this work with houskeeping on return to userspace approach?
- Would we just walk the list of all caches to flush them? could be expensive. Would we somehow note only those that need it? That would make the fast paths do something extra?
- If some other CPU executed
kmem_cache_destroy(), it would have to wait for the isolated cpu returning to userspace. Do we have the means for synchronizing on that? Would that risk a deadlock? We used to have a deferred finishing of the destroy for other reasons but were glad to get rid of it when it was possible, now it might be necessary to revive it?
I don't think you can expect system calls to return to userspace in a given amount of time. Could be in kernel mode for long periods of time.
Quoted 23 lines
How would this work with QPW?
- probably fast paths more expensive due to spin lock vs
local_trylock_t-
flush_rcu_sheaves_on_cache()needs to be solved safely (see above)What if we avoid percpu sheaves completely on isolated cpus and instead allocate/free using the slowpaths?
- It could probably be achieved without affecting fastpaths, as we already handle bootstrap without sheaves, so it's implemented in a way to not affect fastpaths.
- Would it slow the isolcpu workloads down too much when they do a syscall?
- compared to "houskeeping on return to userspace" flushing, maybe not?
Because in that case the syscall starts with sheaves flushed from previous return, it has to do something expensive to get the initial sheaf, then maybe will use only on or few objects, then on return has to flush everything. Likely the slowpath might be faster, unless it allocates/frees many objects from the same cache.
- compared to QPW - it would be slower as QPW would mostly retain sheaves
populated, the need for flushes should be very rare
Quoted 3 lines
So if we can assume that workloads on isolated cpus make syscalls only rarely, and when they do they can tolerate them being slower, I think the "avoid sheaves on isolated cpus" would be the best way here.
I am not sure its safe to assume that. Ask Gemini about isolcpus use cases and:
1. High-Frequency Trading (HFT)
In the world of HFT, microseconds are the difference between profit and loss. Traders use isolcpus to pin their execution engines to specific cores.
The Goal: Eliminate "jitter" caused by the OS moving other processes onto the same core.
The Benefit: Guaranteed execution time and ultra-low latency.
2. Real-Time Audio & Video Processing
If you are running a Digital Audio Workstation (DAW) or a live video encoding rig, a tiny "hiccup" in CPU availability results in an audible pop or a dropped frame.
The Goal: Reserve cores specifically for the Digital Signal Processor (DSP) or the encoder.
The Benefit: Smooth, glitch-free media streams even when the rest of the system is busy.
3. Network Function Virtualization (NFV) & DPDK
For high-speed networking (like 10Gbps+ traffic), the Data Plane Development Kit (DPDK) uses "poll mode" drivers. These drivers constantly loop to check for new packets rather than waiting for interrupts.
The Goal: Isolate cores so they can run at 100% utilization just checking for network packets.
The Benefit: Maximum throughput and zero packet loss in high-traffic environments.
4. Gaming & Simulation
Competitive gamers or flight simulator enthusiasts sometimes isolate a few cores to handle the game's main thread, while leaving the rest of the OS (Discord, Chrome, etc.) to the remaining cores.
The Goal: Prevent background Windows/Linux tasks from stealing cycles from the game engine.
The Benefit: More consistent 1% low FPS and reduced input lag.
5. Deterministic Scientific Computing
If you're running a simulation that needs to take exactly the same amount of time every time it runs (for benchmarking or safety-critical testing), you can't have the OS interference messing with your metrics.
The Goal: Remove the variability of the Linux scheduler.
The Benefit: Highly repeatable, deterministic results.
===
For example, AF_XDP bypass uses system calls (and wants isolcpus):
Quoted 4 lines · Marcelo Tosatti
Only call
rcu_free_sheaf_nobarnifpcs->rcu_freeis not NULL.So it seems safe?
I guess it is.
Quoted 39 lines
Quoted 11 lines
How would this work with houskeeping on return to userspace approach?
- Would we just walk the list of all caches to flush them? could be expensive. Would we somehow note only those that need it? That would make the fast paths do something extra?
- If some other CPU executed
kmem_cache_destroy(), it would have to wait for the isolated cpu returning to userspace. Do we have the means for synchronizing on that? Would that risk a deadlock? We used to have a deferred finishing of the destroy for other reasons but were glad to get rid of it when it was possible, now it might be necessary to revive it?I don't think you can expect system calls to return to userspace in a given amount of time. Could be in kernel mode for long periods of time.
Quoted 23 lines
How would this work with QPW?
- probably fast paths more expensive due to spin lock vs
local_trylock_t-
flush_rcu_sheaves_on_cache()needs to be solved safely (see above)What if we avoid percpu sheaves completely on isolated cpus and instead allocate/free using the slowpaths?
- It could probably be achieved without affecting fastpaths, as we already handle bootstrap without sheaves, so it's implemented in a way to not affect fastpaths.
- Would it slow the isolcpu workloads down too much when they do a syscall?
- compared to "houskeeping on return to userspace" flushing, maybe not?
Because in that case the syscall starts with sheaves flushed from previous return, it has to do something expensive to get the initial sheaf, then maybe will use only on or few objects, then on return has to flush everything. Likely the slowpath might be faster, unless it allocates/frees many objects from the same cache.
- compared to QPW - it would be slower as QPW would mostly retain sheaves
populated, the need for flushes should be very rare
Quoted 6 lines
Quoted 3 lines
So if we can assume that workloads on isolated cpus make syscalls only rarely, and when they do they can tolerate them being slower, I think the "avoid sheaves on isolated cpus" would be the best way here.
I am not sure its safe to assume that. Ask Gemini about isolcpus use cases and:
I don't think it's answering the question about syscalls. But didn't read too closely given the nature of it.
Quoted 4 lines
For example,
AF_XDPbypass uses system calls (and wants isolcpus):
Didn't spot system calls mentioned TBH.
Quoted 51 lines · Vlastimil Babka
Quoted 4 lines · Marcelo Tosatti
Only call
rcu_free_sheaf_nobarnifpcs->rcu_freeis not NULL.So it seems safe?
I guess it is.
Quoted 43 lines
Quoted 11 lines
How would this work with houskeeping on return to userspace approach?
- Would we just walk the list of all caches to flush them? could be expensive. Would we somehow note only those that need it? That would make the fast paths do something extra?
- If some other CPU executed
kmem_cache_destroy(), it would have to wait for the isolated cpu returning to userspace. Do we have the means for synchronizing on that? Would that risk a deadlock? We used to have a deferred finishing of the destroy for other reasons but were glad to get rid of it when it was possible, now it might be necessary to revive it?I don't think you can expect system calls to return to userspace in a given amount of time. Could be in kernel mode for long periods of time.
Quoted 26 lines
How would this work with QPW?
- probably fast paths more expensive due to spin lock vs
local_trylock_t-
flush_rcu_sheaves_on_cache()needs to be solved safely (see above)What if we avoid percpu sheaves completely on isolated cpus and instead allocate/free using the slowpaths?
- It could probably be achieved without affecting fastpaths, as we already handle bootstrap without sheaves, so it's implemented in a way to not affect fastpaths.
- Would it slow the isolcpu workloads down too much when they do a syscall?
- compared to "houskeeping on return to userspace" flushing, maybe not?
Because in that case the syscall starts with sheaves flushed from previous return, it has to do something expensive to get the initial sheaf, then maybe will use only on or few objects, then on return has to flush everything. Likely the slowpath might be faster, unless it allocates/frees many objects from the same cache.
- compared to QPW - it would be slower as QPW would mostly retain sheaves
populated, the need for flushes should be very rareSo if we can assume that workloads on isolated cpus make syscalls only rarely, and when they do they can tolerate them being slower, I think the "avoid sheaves on isolated cpus" would be the best way here.
Quoted 5 lines
Quoted 2 lines
I am not sure its safe to assume that. Ask Gemini about isolcpus use cases and:
I don't think it's answering the question about syscalls. But didn't read too closely given the nature of it.
People use isolcpus with all kinds of programs.
Quoted 5 lines
Quoted 3 lines
For example,
AF_XDPbypass uses system calls (and wants isolcpus):Didn't spot system calls mentioned TBH.
I don't see why you want to reduce performance of applications that execute on isolcpus=, if you can avoid that.
Also, won't bypassing the per-CPU caches increase contention on the global locks, say kmem_cache_node->list_lock.
But if you prefer disabling the per-CPU caches for isolcpus (or a separate option other than isolcpus), then see if people complain about that... works for me.
Two examples:
1)
https://github.com/xdp-project/bpf-examples/blob/main/AF_XDP-example/README.org
Busy-Poll mode
Busy-poll mode. In this mode both the application and the driver can be run efficiently on the same core. The kernel driver is explicitly invoked by the application by calling either recvmsg() or sendto(). Invoke this by setting the -B option. The -b option can be used to set the batch size that the driver will use. For example:
sudo taskset -c 2 ./xdpsock -i <interface> -q 2 -l -N -B -b 256
2)
https://vstinner.github.io/journey-to-stable-benchmark-system.html
Example of effect of CPU isolation on a microbenchmark Example with Linux parameters:
isolcpus=2,3,6,7 nohz_full=2,3,6,7 Microbenchmark on an idle system (without CPU isolation):
$ python3 -m timeit 'sum(range(10**7))' 10 loops, best of 3: 229 msec per loop Result on a busy system using system_load.py 10 and find / commands running in other terminals:
$ python3 -m timeit 'sum(range(10**7))' 10 loops, best of 3: 372 msec per loop The microbenchmark is 56% slower because of the high system load!
Result on the same busy system but using isolated CPUs. The taskset command allows to pin an application to specific CPUs:
$ taskset -c 1,3 python3 -m timeit 'sum(range(10**7))' 10 loops, best of 3: 230 msec per loop Just to check, new run without CPU isolation:
$ python3 -m timeit 'sum(range(10**7))' 10 loops, best of 3: 357 msec per loop The result with CPU isolation on a busy system is the same than the result an idle system! CPU isolation removes most of the noise of the system.
Michal Hocko
reviewerQuoted 1 line · Marcelo Tosatti
On Fri, Feb 20, 2026 at 06:58:10PM +0100, Vlastimil Babka wrote:
[...]
Quoted 12 lines
Quoted 9 lines
Quoted 6 lines
Quoted 3 lines
So if we can assume that workloads on isolated cpus make syscalls only rarely, and when they do they can tolerate them being slower, I think the "avoid sheaves on isolated cpus" would be the best way here.
I am not sure its safe to assume that. Ask Gemini about isolcpus use cases and:
I don't think it's answering the question about syscalls. But didn't read too closely given the nature of it.
People use isolcpus with all kinds of programs.
Quoted 8 lines
Quoted 5 lines
Quoted 3 lines
For example,
AF_XDPbypass uses system calls (and wants isolcpus):Didn't spot system calls mentioned TBH.
I don't see why you want to reduce performance of applications that execute on isolcpus=, if you can avoid that.
If you can avoid that by making performance bad for everybody else then then it seems safer to sacrifice those workloads that are much more special - i.e. cpu isolation.
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 19 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Fri, Feb 20, 2026 at 06:58:10PM +0100, Vlastimil Babka wrote:
[...]
Quoted 16 lines
Quoted 9 lines
Quoted 6 lines
Quoted 3 lines
So if we can assume that workloads on isolated cpus make syscalls only rarely, and when they do they can tolerate them being slower, I think the "avoid sheaves on isolated cpus" would be the best way here.
I am not sure its safe to assume that. Ask Gemini about isolcpus use cases and:
I don't think it's answering the question about syscalls. But didn't read too closely given the nature of it.
People use isolcpus with all kinds of programs.
Quoted 4 lines
Quoted 3 lines
For example,
AF_XDPbypass uses system calls (and wants isolcpus):
Quoted 11 lines
Quoted 4 lines
Quoted 1 line
Didn't spot system calls mentioned TBH.
I don't see why you want to reduce performance of applications that execute on isolcpus=, if you can avoid that.
If you can avoid that by making performance bad for everybody else then then it seems safer to sacrifice those workloads that are much more special - i.e. cpu isolation.
Signature · 2 lines
Michal Hocko SUSE Labs
Performance is not bad for everyone else:
Without patchset:
================
[ 1188.050725] kmalloc_bench: Avg cycles per kmalloc: 159
With qpw patchset, CONFIG_QPW=n:
================================
[ 50.292190] kmalloc_bench: Avg cycles per kmalloc: 163
And its probably possible to remove those 4 cycles.
Which makes reduction of performance of isolcpus not necessary.
Quoted 2 lines · Marcelo Tosatti
I am not sure its safe to assume that. Ask Gemini about isolcpus use
Erm... ok fine let's see that :-)
Quoted 9 lines
cases and:
1. High-Frequency Trading (HFT)
In the world of HFT, microseconds are the difference between profit and loss. Traders use isolcpus to pin their execution engines to specific cores.The Goal: Eliminate "jitter" caused by the OS moving other processes onto the same core.
The Benefit: Guaranteed execution time and ultra-low latency.
That would be full isolation (aka nohz_full) because the goal here is to beat the competitors. As such the software latency must tend toward hardware latency.
I wouldn't expect any syscall here but a full userspace stack with DPDK for example.
I put that in the 5g uRLLC (or similar low latency networking) usecase family.
Quoted 8 lines
2. Real-Time Audio & Video Processing
If you are running a Digital Audio Workstation (DAW) or a live video encoding rig, a tiny "hiccup" in CPU availability results in an audible pop or a dropped frame.The Goal: Reserve cores specifically for the Digital Signal Processor (DSP) or the encoder.
The Benefit: Smooth, glitch-free media streams even when the rest of the system is busy.
Here I expect weaker isolation requirements with syscalls involved. Scheduler domain isolation alone (aka isolcpus=[domain]) would fit.
Quoted 8 lines
3. Network Function Virtualization (NFV) & DPDK
For high-speed networking (like 10Gbps+ traffic), the Data Plane Development Kit (DPDK) uses "poll mode" drivers. These drivers constantly loop to check for new packets rather than waiting for interrupts.The Goal: Isolate cores so they can run at 100% utilization just checking for network packets.
The Benefit: Maximum throughput and zero packet loss in high-traffic environments.
I put that in the 5g uRLLC usecase family as well (again or similar low latency networking).
Quoted 6 lines
4. Gaming & Simulation
Competitive gamers or flight simulator enthusiasts sometimes isolate a few cores to handle the game's main thread, while leaving the rest of the OS (Discord, Chrome, etc.) to the remaining cores.The Goal: Prevent background Windows/Linux tasks from stealing cycles from the game engine.
The Benefit: More consistent 1% low FPS and reduced input lag.
That's domain isolation because frequent syscalls are unavoidable.
Quoted 7 lines
5. Deterministic Scientific Computing
If you're running a simulation that needs to take exactly the same amount of time every time it runs (for benchmarking or safety-critical testing), you can't have the OS interference messing with your metrics.The Goal: Remove the variability of the Linux scheduler.
The Benefit: Highly repeatable, deterministic results.
I guess here there are plenty of flavours. The only one I know of is this power simulator that relies of nohz_full. Not sure about the implementation relying on syscalls or not:
https://dpsim.fein-aachen.org/docs/getting-started/real-time/
Quoted 3 lines
For example,
AF_XDPbypass uses system calls (and wants isolcpus):
That's HFT again and they state that they rely on polling userspace drivers so I don't expect syscalls.
But anyway here is a summary I would propose:
* Domain isolation alone is a good fit when some glitches must be avoided but kernel work is still necessary: non critical high volume networking or data capture, video games, etc...
* Full isolation is a better fit for ultra low latency requirement, in this case the kernel is only good for preparatory work and interface layout between userspace and the hardware (VFIO).
I've observed 3 patterns so far:
- Low latency networking with DPDK, eg: 5g uRLLC (should be syscalls free)
- Scientific simulation (not sure about syscalls)
- HPC computation such as LLM (not sure about syscalls).
Is flushing work only relevant for full isolation? If so I can't say which is the best solution between flushing pending work on syscall exit and doing that remotely. But if it's relevant also for domain isolation, then the remote work is better because it doesn't add unecessary work on syscalls which still happen in this mode.
At least doing things remotely should be free of any surprising side-effects. But we must determine how to properly activate the isolated mode (switch to spinlocks) depending on the isolation mode which can be not only defined on boot but also on runtime (at least for domain isolation through cpusets but it will be the case as well with nohz_full in the future).
Thanks.
Signature · 2 lines
Frederic Weisbecker SUSE Labs
Quoted 93 lines · Frederic Weisbecker
Quoted 2 lines · Marcelo Tosatti
I am not sure its safe to assume that. Ask Gemini about isolcpus use
Erm... ok fine let's see that :-)
Quoted 9 lines
cases and:
1. High-Frequency Trading (HFT)
In the world of HFT, microseconds are the difference between profit and loss. Traders use isolcpus to pin their execution engines to specific cores.The Goal: Eliminate "jitter" caused by the OS moving other processes onto the same core.
The Benefit: Guaranteed execution time and ultra-low latency.
That would be full isolation (aka
nohz_full) because the goal here is to beat the competitors. As such the software latency must tend toward hardware latency.I wouldn't expect any syscall here but a full userspace stack with DPDK for example.
I put that in the 5g uRLLC (or similar low latency networking) usecase family.
Quoted 8 lines
2. Real-Time Audio & Video Processing
If you are running a Digital Audio Workstation (DAW) or a live video encoding rig, a tiny "hiccup" in CPU availability results in an audible pop or a dropped frame.The Goal: Reserve cores specifically for the Digital Signal Processor (DSP) or the encoder.
The Benefit: Smooth, glitch-free media streams even when the rest of the system is busy.
Here I expect weaker isolation requirements with syscalls involved. Scheduler domain isolation alone (aka isolcpus=[domain]) would fit.
Quoted 8 lines
3. Network Function Virtualization (NFV) & DPDK
For high-speed networking (like 10Gbps+ traffic), the Data Plane Development Kit (DPDK) uses "poll mode" drivers. These drivers constantly loop to check for new packets rather than waiting for interrupts.The Goal: Isolate cores so they can run at 100% utilization just checking for network packets.
The Benefit: Maximum throughput and zero packet loss in high-traffic environments.
I put that in the 5g uRLLC usecase family as well (again or similar low latency networking).
Quoted 6 lines
4. Gaming & Simulation
Competitive gamers or flight simulator enthusiasts sometimes isolate a few cores to handle the game's main thread, while leaving the rest of the OS (Discord, Chrome, etc.) to the remaining cores.The Goal: Prevent background Windows/Linux tasks from stealing cycles from the game engine.
The Benefit: More consistent 1% low FPS and reduced input lag.
That's domain isolation because frequent syscalls are unavoidable.
Quoted 7 lines
5. Deterministic Scientific Computing
If you're running a simulation that needs to take exactly the same amount of time every time it runs (for benchmarking or safety-critical testing), you can't have the OS interference messing with your metrics.The Goal: Remove the variability of the Linux scheduler.
The Benefit: Highly repeatable, deterministic results.
I guess here there are plenty of flavours. The only one I know of is this power simulator that relies of
nohz_full. Not sure about the implementation relying on syscalls or not:https://dpsim.fein-aachen.org/docs/getting-started/real-time/
Quoted 3 lines
For example,
AF_XDPbypass uses system calls (and wants isolcpus):That's HFT again and they state that they rely on polling userspace drivers so I don't expect syscalls.
But anyway here is a summary I would propose:
* Domain isolation alone is a good fit when some glitches must be avoided but kernel work is still necessary: non critical high volume networking or data capture, video games, etc...
* Full isolation is a better fit for ultra low latency requirement, in this case the kernel is only good for preparatory work and interface layout between userspace and the hardware (VFIO).
I've observed 3 patterns so far:
- Low latency networking with DPDK, eg: 5g uRLLC (should be syscalls free)
- Scientific simulation (not sure about syscalls)
- HPC computation such as LLM (not sure about syscalls).
Quoted 5 lines
Is flushing work only relevant for full isolation? If so I can't say which is the best solution between flushing pending work on syscall exit and doing that remotely. But if it's relevant also for domain isolation, then the remote work is better because it doesn't add unecessary work on syscalls which still happen in this mode.
Yes, see my last email about HPC.
Quoted 7 lines
At least doing things remotely should be free of any surprising side-effects. But we must determine how to properly activate the isolated mode (switch to spinlocks) depending on the isolation mode which can be not only defined on boot but also on runtime (at least for domain isolation through cpusets but it will be the case as well with
nohz_fullin the future).Thanks.
If you boot with remote spinlocks (qpw=1) today, then you can't change that.
You could, because its a static key:
#define qpw_lock(lock, cpu) \
do { \
if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \
spin_lock(per_cpu_ptr(lock.sl, cpu)); \
else \
local_lock(lock.ll); \
} while (0)But haven't thought about switching on runtime (and don't see why it would be necessary to switch on runtime). It is independent of switching CPUs to/from being isolated (or nohz_full).
OK will address the remaining comments and repost.
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 8 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big. To me, a much more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.
Without patchset:
================
[ 1188.050725] kmalloc_bench: Avg cycles per kmalloc: 159
With qpw patchset, CONFIG_QPW=n:
================================
[ 50.292190] kmalloc_bench: Avg cycles per kmalloc: 163
With qpw patchset, CONFIG_QPW=y, qpw=0:
=======================================
[ 29.872153] kmalloc_bench: Avg cycles per kmalloc: 170
With qpw patchset, CONFIG_QPW=y, qpw=1:
========================================
[ 37.494687] kmalloc_bench: Avg cycles per kmalloc: 190
With PREEMPT_RT enabled, qpw=0:
===============================
[ 65.163251] kmalloc_bench: Avg cycles per kmalloc: 181
With PREEMPT_RT enabled, no patchset:
=====================================
[ 52.701639] kmalloc_bench: Avg cycles per kmalloc: 185
With PREEMPT_RT enabled, qpw=1:
==============================
[ 35.103830] kmalloc_bench: Avg cycles per kmalloc: 196
Quoted 99 lines · Marcelo Tosatti
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 8 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big. To me, a much more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.
Without patchset:
================[ 1188.050725] kmalloc_bench: Avg cycles per kmalloc: 159With qpw patchset,
CONFIG_QPW=n:
================================[ 50.292190] kmalloc_bench: Avg cycles per kmalloc: 163With qpw patchset,
CONFIG_QPW=y, qpw=0:
=======================================[ 29.872153] kmalloc_bench: Avg cycles per kmalloc: 170With qpw patchset,
CONFIG_QPW=y, qpw=1:
========================================[ 37.494687] kmalloc_bench: Avg cycles per kmalloc: 190With
PREEMPT_RTenabled, qpw=0:
===============================[ 65.163251] kmalloc_bench: Avg cycles per kmalloc: 181
Quoted 8 lines
With
PREEMPT_RTenabled, no patchset:
=====================================[ 52.701639] kmalloc_bench: Avg cycles per kmalloc: 185With
PREEMPT_RTenabled, qpw=1:
==============================[ 35.103830] kmalloc_bench: Avg cycles per kmalloc: 196
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/timex.h>
#include <linux/preempt.h>
#include <linux/irqflags.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");MODULE_AUTHOR("Gemini AI");MODULE_DESCRIPTION("A simple kmalloc performance benchmark");
static int size = 64; // Default allocation size in bytes module_param(size, int, 0644);
static int iterations = 1000000; // Default number of iterations module_param(iterations, int, 0644);
static int __init kmalloc_bench_init(void) {
void **ptrs;
cycles_t start, end;
uint64_t total_cycles;
int i;
pr_info("kmalloc_bench: Starting test (size=%d, iterations=%d)\n", size, iterations);
// Allocate an array to store pointers to avoid immediate kfree-reuse optimization
ptrs = vmalloc(sizeof(void *) * iterations);
if (!ptrs) {
pr_err("kmalloc_bench: Failed to allocate pointer array\n");
return -ENOMEM;
}
preempt_disable();
start = get_cycles();
for (i = 0; i < iterations; i++) {
ptrs[i] = kmalloc(size, GFP_ATOMIC);
}
end = get_cycles();
total_cycles = end - start;
preempt_enable();
pr_info("kmalloc_bench: Total cycles for %d allocs: %llu\n", iterations, total_cycles);
pr_info("kmalloc_bench: Avg cycles per kmalloc: %llu\n", total_cycles / iterations);
// Cleanup
for (i = 0; i < iterations; i++) {
kfree(ptrs[i]);
}
vfree(ptrs);
return 0;
}static void __exit kmalloc_bench_exit(void) {
pr_info("kmalloc_bench: Module unloaded\n");
}Quoted 77 lines · Marcelo Tosatti
Quoted 76 lines · Marcelo Tosatti
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 8 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big. To me, a much more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.
Without patchset:
================
Quoted 6 lines
Quoted 6 lines
[ 1188.050725] kmalloc_bench: Avg cycles per kmalloc: 159With qpw patchset,
CONFIG_QPW=n:
================================[ 50.292190] kmalloc_bench: Avg cycles per kmalloc: 163
Weird.. with CONFIG_QPW we should see no difference.
Oh, maybe the changes in the code, such as adding a new cpu parameter in some functions may have caused this.
(oh, there is the migrate_disable as well)
Quoted 6 lines
Quoted 6 lines
With qpw patchset,
CONFIG_QPW=y, qpw=0:
=======================================[ 29.872153] kmalloc_bench: Avg cycles per kmalloc: 170
Humm, what changed here is basically from
+#define qpw_lock(lock, cpu) \
+ local_lock(lock)
to
+#define qpw_lock(lock, cpu) \
+ do { \
+ if (static_branch_maybe(CONFIG_QPW_DEFAULT, &qpw_sl)) \
+ spin_lock(per_cpu_ptr(lock.sl, cpu)); \
+ else \
+ local_lock(lock.ll); \
+ } while (0)
So only the cost of a static branch.. maybe I did something wrong here with the static_branch_maybe, as any cpu branch predictor should make this delta close to zero.
Quoted 6 lines
Quoted 6 lines
With qpw patchset,
CONFIG_QPW=y, qpw=1:
========================================[ 37.494687] kmalloc_bench: Avg cycles per kmalloc: 190
20 cycles as a price for a local_lock->spinlock seems too much. Taking in account the previous message, maybe we should work on making them inlined spinlocks, if not already.
(Yeah, I missed that verification :| )
Quoted 9 lines
Quoted 9 lines
With
PREEMPT_RTenabled, qpw=0:
===============================[ 65.163251] kmalloc_bench: Avg cycles per kmalloc: 181With
PREEMPT_RTenabled, no patchset:
=====================================[ 52.701639] kmalloc_bench: Avg cycles per kmalloc: 185
Nice, having the QPW patch saved some cycles :)
Quoted 5 lines
Quoted 4 lines
With
PREEMPT_RTenabled, qpw=1:
==============================[ 35.103830] kmalloc_bench: Avg cycles per kmalloc: 196
This is odd, though. The spinlock is already there, so from qpw=0 to qpw=1 there should be no performance change. Maybe in local_lock they do some optimization in their spinlock?
Quoted 56 lines
#include <
linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/timex.h>
#include <linux/preempt.h>
#include <linux/irqflags.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");MODULE_AUTHOR("Gemini AI");MODULE_DESCRIPTION("A simple kmalloc performance benchmark");static int size = 64; // Default allocation size in bytes
module_param(size, int, 0644);static int iterations = 1000000; // Default number of iterations
module_param(iterations, int, 0644);static int __init kmalloc_bench_init(void) { void **ptrs; cycles_t start, end; uint64_t total_cycles; int i; pr_info("kmalloc_bench: Starting test (size=%d, iterations=%d)\n", size, iterations); // Allocate an array to store pointers to avoid immediate kfree-reuse optimization ptrs = vmalloc(sizeof(void *) * iterations); if (!ptrs) { pr_err("kmalloc_bench: Failed to allocate pointer array\n"); return -ENOMEM; } preempt_disable(); start = get_cycles(); for (i = 0; i < iterations; i++) { ptrs[i] = kmalloc(size, GFP_ATOMIC); } end = get_cycles(); total_cycles = end - start; preempt_enable(); pr_info("kmalloc_bench: Total cycles for %d allocs: %llu\n", iterations, total_cycles); pr_info("kmalloc_bench: Avg cycles per kmalloc: %llu\n", total_cycles / iterations); // Cleanup for (i = 0; i < iterations; i++) { kfree(ptrs[i]); } vfree(ptrs); return 0; }
Quoted 5 lines
static void __exit kmalloc_bench_exit(void) {
pr_info("kmalloc_bench: Module unloaded\n");
}Nice!
Please collect min and max as well, maybe we can have an insight of what could have happened, then :)
What was the system you used for testing?
Thanks!
Leo
Quoted 36 lines · Marcelo Tosatti
#include <
linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/timex.h>
#include <linux/preempt.h>
#include <linux/irqflags.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");MODULE_AUTHOR("Gemini AI");MODULE_DESCRIPTION("A simple kmalloc performance benchmark");static int size = 64; // Default allocation size in bytes
module_param(size, int, 0644);static int iterations = 1000000; // Default number of iterations
module_param(iterations, int, 0644);static int __init kmalloc_bench_init(void) { void **ptrs; cycles_t start, end; uint64_t total_cycles; int i; pr_info("kmalloc_bench: Starting test (size=%d, iterations=%d)\n", size, iterations); // Allocate an array to store pointers to avoid immediate kfree-reuse optimization ptrs = vmalloc(sizeof(void *) * iterations); if (!ptrs) { pr_err("kmalloc_bench: Failed to allocate pointer array\n"); return -ENOMEM; } preempt_disable(); start = get_cycles();
Quoted 8 lines
for (i = 0; i < iterations; i++) {
ptrs[i] = kmalloc(size, GFP_ATOMIC);
}
end = get_cycles();
total_cycles = end - start;
preempt_enable();While preempt_disable() simplifies things, it can misrepresent the cost of preempt_disable() that's part of the locking - that will become nested and then the nested preempt_disable() is typically cheaper, etc.
Also the way it kmallocs all iterations and then kfree all iterations may skew the probabilities of fastpaths, cache hotness etc.
When introducing sheaves I had a similar microbenchmark, but there was different amounts of inner-loop iteraions, no outer preempt_disable(), and linear vs randomized array. See:
(at this point the SLUB_HAS_SHEAVES parts should be removed and the kmem_cache_print_stats() stuff also shouldn't be interesting for QPW evaluation).
Quoted 18 lines
pr_info("kmalloc_bench: Total cycles for %d allocs: %llu\n", iterations, total_cycles); pr_info("kmalloc_bench: Avg cycles per kmalloc: %llu\n", total_cycles / iterations); // Cleanup for (i = 0; i < iterations; i++) { kfree(ptrs[i]); } vfree(ptrs); return 0; }static void __exit kmalloc_bench_exit(void) { pr_info("kmalloc_bench: Module unloaded\n"); }
Quoted 57 lines · Vlastimil Babka
Quoted 36 lines · Marcelo Tosatti
#include <
linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/timex.h>
#include <linux/preempt.h>
#include <linux/irqflags.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");MODULE_AUTHOR("Gemini AI");MODULE_DESCRIPTION("A simple kmalloc performance benchmark");static int size = 64; // Default allocation size in bytes
module_param(size, int, 0644);static int iterations = 1000000; // Default number of iterations
module_param(iterations, int, 0644);static int __init kmalloc_bench_init(void) { void **ptrs; cycles_t start, end; uint64_t total_cycles; int i; pr_info("kmalloc_bench: Starting test (size=%d, iterations=%d)\n", size, iterations); // Allocate an array to store pointers to avoid immediate kfree-reuse optimization ptrs = vmalloc(sizeof(void *) * iterations); if (!ptrs) { pr_err("kmalloc_bench: Failed to allocate pointer array\n"); return -ENOMEM; } preempt_disable(); start = get_cycles();Quoted 8 lines
for (i = 0; i < iterations; i++) { ptrs[i] = kmalloc(size, GFP_ATOMIC); } end = get_cycles(); total_cycles = end - start; preempt_enable();While
preempt_disable()simplifies things, it can misrepresent the cost ofpreempt_disable()that's part of the locking - that will become nested and then the nestedpreempt_disable()is typically cheaper, etc.Also the way it kmallocs all iterations and then kfree all iterations may skew the probabilities of fastpaths, cache hotness etc.
When introducing sheaves I had a similar microbenchmark, but there was different amounts of inner-loop iteraions, no outer
preempt_disable(), and linear vs randomized array. See:
Quoted 5 lines
(at this point the
SLUB_HAS_SHEAVESparts should be removed and thekmem_cache_print_stats()stuff also shouldn't be interesting for QPW evaluation).
Hi Vlastimil,
There is a problem which the numbers vary significantly across runs (on the same kernel, system is idle, cpu is isolated).
SLUB_HAS_SHEAVES is not defined on my build. Just copied slub_kunit.c from slub-percpu-sheaves-v6-benchmarking
to current tip (and dropped call to kmem_cache_print_stats).
1st run:
[ 635.059928] average (excl. iter 0): 56571797 [ 635.235206] average (excl. iter 0): 58329901 [ 635.409957] average (excl. iter 0): 57459678 [ 635.585128] average (excl. iter 0): 58268333 [ 635.767325] average (excl. iter 0): 60063837 [ 635.944534] average (excl. iter 0): 58912817 [ 636.154503] average (excl. iter 0): 68992131 [ 636.362533] average (excl. iter 0): 69030629 [ 636.536737] average (excl. iter 0): 56545622 [ 636.704314] average (excl. iter 0): 55536407 [ 636.879097] average (excl. iter 0): 57397803 [ 637.051157] average (excl. iter 0): 57021907 [ 637.296352] average (excl. iter 0): 81582815 [ 637.539810] average (excl. iter 0): 81126686
2nd run:
[ 662.824688] average (excl. iter 0): 56833529 [ 662.996742] average (excl. iter 0): 57145388 [ 663.167063] average (excl. iter 0): 55828870 [ 663.339814] average (excl. iter 0): 57505312 [ 663.514563] average (excl. iter 0): 57374528 [ 663.690328] average (excl. iter 0): 57282062 [ 663.896128] average (excl. iter 0): 68097440 [ 664.103029] average (excl. iter 0): 69263914 [ 664.276497] average (excl. iter 0): 57073271 [ 664.442210] average (excl. iter 0): 54895879 [ 664.617186] average (excl. iter 0): 56972700 [ 664.787353] average (excl. iter 0): 56457173 [ 665.028944] average (excl. iter 0): 80339269 [ 665.268597] average (excl. iter 0): 80371907
3rd run:
[ 716.278750] average (excl. iter 0): 54191777 [ 716.442014] average (excl. iter 0): 54151132 [ 716.605254] average (excl. iter 0): 53148722 [ 716.766461] average (excl. iter 0): 53204894 [ 716.933339] average (excl. iter 0): 54719251 [ 717.098761] average (excl. iter 0): 54922923 [ 717.296178] average (excl. iter 0): 65351864 [ 717.491440] average (excl. iter 0): 65264027 [ 717.660778] average (excl. iter 0): 54370768 [ 717.823625] average (excl. iter 0): 54137410 [ 717.988983] average (excl. iter 0): 54222488 [ 718.152716] average (excl. iter 0): 54339019 [ 718.387978] average (excl. iter 0): 78249026 [ 718.619598] average (excl. iter 0): 77746198
Increasing total parameter from 10^6 to 10^7 does not help:
1st run:
[ 1074.601686] average (excl. iter 0): 650711901 [ 1076.450880] average (excl. iter 0): 633014260 [ 1078.363300] average (excl. iter 0): 660440649 [ 1080.266134] average (excl. iter 0): 652695083 [ 1082.117007] average (excl. iter 0): 635632144 [ 1084.009277] average (excl. iter 0): 654270513 [ 1086.286343] average (excl. iter 0): 790520038 [ 1088.512516] average (excl. iter 0): 768071705 [ 1090.448161] average (excl. iter 0): 664564330 [ 1092.349683] average (excl. iter 0): 659016349 [ 1094.274099] average (excl. iter 0): 662388982 [ 1096.172362] average (excl. iter 0): 647972747 [ 1098.753304] average (excl. iter 0): 887576313 [ 1101.339897] average (excl. iter 0): 885102019
2nd run:
[ 1120.186284] average (excl. iter 0): 615756734 [ 1122.019323] average (excl. iter 0): 623846524 [ 1123.885801] average (excl. iter 0): 639124895 [ 1125.693617] average (excl. iter 0): 623667563 [ 1127.588515] average (excl. iter 0): 646441510 [ 1129.410285] average (excl. iter 0): 628291996 [ 1131.542157] average (excl. iter 0): 728497604 [ 1133.698744] average (excl. iter 0): 743717953 [ 1135.514112] average (excl. iter 0): 616621660 [ 1137.306874] average (excl. iter 0): 615863807 [ 1139.110637] average (excl. iter 0): 616425899 [ 1140.948769] average (excl. iter 0): 638115570 [ 1143.426557] average (excl. iter 0): 847799304 [ 1145.914827] average (excl. iter 0): 861180802
Will switch back to the simple test (and its pretty obvious from the patch itself that if qpw=0 the overhead should be zero, and it is). Its numbers are more
stable across runs.
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 4 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big.
Awesome! Thanks for reviewing!
Quoted 5 lines
To me, a much
more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.
That crossed my mind as well, and I went with the idea of changing locking because I was working on workloads in which deferring work to a kernel re-entry would cause deadline misses as well. Or more critically, the drains could take forever, as some of those tasks would avoid returning to kernel as much as possible.
Thanks!
Leo
Michal Hocko
reviewerQuoted 77 lines · Leonardo Bras
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 4 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big.
Awesome! Thanks for reviewing!
Quoted 5 lines
To me, a much
more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.
Quoted 5 lines
That crossed my mind as well, and I went with the idea of changing locking because I was working on workloads in which deferring work to a kernel re-entry would cause deadline misses as well. Or more critically, the drains could take forever, as some of those tasks would avoid returning to kernel as much as possible.
Could you be more specific please?
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 78 lines · Michal Hocko
Quoted 77 lines · Leonardo Bras
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 4 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big.
Awesome! Thanks for reviewing!
Quoted 5 lines
To me, a much
more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.
Quoted 7 lines
Quoted 5 lines
That crossed my mind as well, and I went with the idea of changing locking because I was working on workloads in which deferring work to a kernel re-entry would cause deadline misses as well. Or more critically, the drains could take forever, as some of those tasks would avoid returning to kernel as much as possible.
Could you be more specific please?
Hi Michal,
Sorry for the delay
I think Marcelo covered some of the main topics earlier in this thread:
https://lore.kernel.org/all/aZ3ejedS7nE5mnva@tpad/
But in syntax:
- There are workloads that are projected not avoid as much as possible return to kernelspace, as they are either cpu intensive, or latency sensitive (RT workloads) such as low-latency automation.
There are scenarios such as industrial automation in which
the applications are supposed to reply a request in less than 50us since it was generated (IIRC), so sched-out, dealing with interruptions, or syscalls are a no-go. In those cases, using cpu isolation is a must, and since it can stay really long running in userspace, it may take a very long time to do any syscall to actually perform the scheduled flush.
- Other workloads may need to use syscalls, or rely in interrupts, such as HPC, but it's also not interesting to take long on them, as the time spent there is time not used for processing the required data.
Let's say that for the sake of cpu isolation, a lot of different requests made to given isolated cpu are batched to be run on syscall entry/exit. It means the next syscall may take much longer than usual.
- This may break other RT workloads such as sensor/sound/image sampling, which could be generally ok with some of the faster syscalls for their application, and now may perceive an error because one of those syscalls took too long.
While the qpw approach may cost a few extra cycles, it operates remotelly and makes the system a bit more predictable.
Also, when I was planning the mechanism, I remember it was meant to add zero overhead in case of CONFIG_QPW=n, very little overhead in case of CONFIG_QPW=y + qpw=0 (a couple of static branches, possibly with the cost removed by the cpu branch predictor), and only add a few cycles in case of qpw=1 + !RT. Which means we may be missing just a few adjustments to get there.
BTW, if the numbers are not that great for your workloads, we could take a look at adding an extra QPW mode in which local_locks are taken in the fastpath and it allows the flush wq to be posponed to that point in syscall return that you mentioned. What I mean is that we don't need to be limitted to choosing between solutions, but instead allow the user (or distro) to choose the desired behavior.
Thanks!
Leo
Quoted 123 lines · Leonardo Bras
Quoted 78 lines · Michal Hocko
Quoted 77 lines · Leonardo Bras
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 4 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big.
Awesome! Thanks for reviewing!
Quoted 5 lines
To me, a much
more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.Quoted 7 lines
Quoted 5 lines
That crossed my mind as well, and I went with the idea of changing locking because I was working on workloads in which deferring work to a kernel re-entry would cause deadline misses as well. Or more critically, the drains could take forever, as some of those tasks would avoid returning to kernel as much as possible.
Could you be more specific please?
Hi Michal,
Sorry for the delayI think Marcelo covered some of the main topics earlier in this thread:
https://lore.kernel.org/all/aZ3ejedS7nE5mnva@tpad/
But in syntax:
- There are workloads that are projected not avoid as much as possible return to kernelspace, as they are either cpu intensive, or latency sensitive (RT workloads) such as low-latency automation.There are scenarios such as industrial automation in which
the applications are supposed to reply a request in less than 50us since it was generated (IIRC), so sched-out, dealing with interruptions, or syscalls are a no-go. In those cases, using cpu isolation is a must, and since it can stay really long running in userspace, it may take a very long time to do any syscall to actually perform the scheduled flush.- Other workloads may need to use syscalls, or rely in interrupts, such as HPC, but it's also not interesting to take long on them, as the time spent there is time not used for processing the required data.
Let's say that for the sake of cpu isolation, a lot of different requests made to given isolated cpu are batched to be run on syscall entry/exit. It means the next syscall may take much longer than usual.
- This may break other RT workloads such as sensor/sound/image sampling, which could be generally ok with some of the faster syscalls for their application, and now may perceive an error because one of those syscalls took too long.While the qpw approach may cost a few extra cycles, it operates remotelly and makes the system a bit more predictable.
Quoted 6 lines
Also, when I was planning the mechanism, I remember it was meant to add zero overhead in case of
CONFIG_QPW=n, very little overhead in case ofCONFIG_QPW=y+ qpw=0 (a couple of static branches, possibly with the cost removed by the cpu branch predictor), and only add a few cycles in case of qpw=1 + !RT. Which means we may be missing just a few adjustments to get there.
Leo,
v2 of the patchset adds only 2 cycles to CONFIG_QPW=y + qpw=0. The larger overhead was due to migrate_disable, which is now (on v2) hidden inside the static branch.
My bad.
Quoted 9 lines
BTW, if the numbers are not that great for your workloads, we could take a look at adding an extra QPW mode in which
local_locksare taken in the fastpath and it allows the flush wq to be posponed to that point in syscall return that you mentioned. What I mean is that we don't need to be limitted to choosing between solutions, but instead allow the user (or distro) to choose the desired behavior.Thanks!
Leo
I think 2 cycles is acceptable.
Quoted 131 lines · Marcelo Tosatti
Quoted 123 lines · Leonardo Bras
Quoted 78 lines · Michal Hocko
Quoted 77 lines · Leonardo Bras
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 4 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big.
Awesome! Thanks for reviewing!
Quoted 5 lines
To me, a much
more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.Quoted 7 lines
Quoted 5 lines
That crossed my mind as well, and I went with the idea of changing locking because I was working on workloads in which deferring work to a kernel re-entry would cause deadline misses as well. Or more critically, the drains could take forever, as some of those tasks would avoid returning to kernel as much as possible.
Could you be more specific please?
Hi Michal,
Sorry for the delayI think Marcelo covered some of the main topics earlier in this thread:
https://lore.kernel.org/all/aZ3ejedS7nE5mnva@tpad/
But in syntax:
- There are workloads that are projected not avoid as much as possible return to kernelspace, as they are either cpu intensive, or latency sensitive (RT workloads) such as low-latency automation.There are scenarios such as industrial automation in which
the applications are supposed to reply a request in less than 50us since it was generated (IIRC), so sched-out, dealing with interruptions, or syscalls are a no-go. In those cases, using cpu isolation is a must, and since it can stay really long running in userspace, it may take a very long time to do any syscall to actually perform the scheduled flush.- Other workloads may need to use syscalls, or rely in interrupts, such as HPC, but it's also not interesting to take long on them, as the time spent there is time not used for processing the required data.
Let's say that for the sake of cpu isolation, a lot of different requests made to given isolated cpu are batched to be run on syscall entry/exit. It means the next syscall may take much longer than usual.
- This may break other RT workloads such as sensor/sound/image sampling, which could be generally ok with some of the faster syscalls for their application, and now may perceive an error because one of those syscalls took too long.While the qpw approach may cost a few extra cycles, it operates remotelly and makes the system a bit more predictable.
Quoted 6 lines
Also, when I was planning the mechanism, I remember it was meant to add zero overhead in case of
CONFIG_QPW=n, very little overhead in case ofCONFIG_QPW=y+ qpw=0 (a couple of static branches, possibly with the cost removed by the cpu branch predictor), and only add a few cycles in case of qpw=1 + !RT. Which means we may be missing just a few adjustments to get there.
Quoted 6 lines
Leo,
v2 of the patchset adds only 2 cycles to
CONFIG_QPW=y+ qpw=0. The larger overhead was due tomigrate_disable, which is now (on v2) hidden inside the static branch.
My bad.
Hi Marcelo,
Great, hiding migrate_disable under the static branch is the best scenario.
I wonder why we spend 2 cycles on the static branches, though, should be close to nothing unless the branch predictor is too busy already. Well, we can always try to optimize in a different way.
Thanks for the effort on this!
Leo
Quoted 13 lines
Quoted 10 lines
BTW, if the numbers are not that great for your workloads, we could take a look at adding an extra QPW mode in which
local_locksare taken in the fastpath and it allows the flush wq to be posponed to that point in syscall return that you mentioned. What I mean is that we don't need to be limitted to choosing between solutions, but instead allow the user (or distro) to choose the desired behavior.Thanks!
LeoI think 2 cycles is acceptable.
Vlastimil Babka (SUSE)
maintainerQuoted 6 lines · Leonardo Bras
Hi Marcelo,
Great, hiding
migrate_disableunder the static branch is the best scenario.I wonder why we spend 2 cycles on the static branches, though, should be close to nothing unless the branch predictor is too busy already. Well, we
AFAIK static branches are runtime patched to non-conditional jumps or nops, so there's nothing left for the branch predictor to do. Or maybe I misunderstand your comment.
It does however increase code footprint and thus instruction cache usage, so maybe that's an effect of that.
Quoted 4 lines
can always try to optimize in a different way.
Thanks for the effort on this!
Leo
Quoted 5 lines · Vlastimil Babka (SUSE)
Quoted 4 lines · Leonardo Bras
Hi Marcelo,
Great, hiding
migrate_disableunder the static branch is the best scenario.
Quoted 8 lines
Quoted 2 lines
I wonder why we spend 2 cycles on the static branches, though, should be close to nothing unless the branch predictor is too busy already. Well, we
AFAIK static branches are runtime patched to non-conditional jumps or nops, so there's nothing left for the branch predictor to do. Or maybe I misunderstand your comment.
It does however increase code footprint and thus instruction cache usage, so maybe that's an effect of that.
Oh, I was not aware of that part. Since we can actually change the the branch decision variable in runtime, I guessed that it was just a optimized way for the processor to use the branch predictor.
If it makes a non-conditional branch is even better then.
Thanks!
Leo
Quoted 5 lines
Quoted 5 lines
can always try to optimize in a different way.
Thanks for the effort on this!
Leo
Quoted 141 lines · Leonardo Bras
Quoted 131 lines · Marcelo Tosatti
Quoted 123 lines · Leonardo Bras
Quoted 78 lines · Michal Hocko
Quoted 77 lines · Leonardo Bras
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 4 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big.
Awesome! Thanks for reviewing!
Quoted 5 lines
To me, a much
more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.Quoted 7 lines
Quoted 5 lines
That crossed my mind as well, and I went with the idea of changing locking because I was working on workloads in which deferring work to a kernel re-entry would cause deadline misses as well. Or more critically, the drains could take forever, as some of those tasks would avoid returning to kernel as much as possible.
Could you be more specific please?
Hi Michal,
Sorry for the delayI think Marcelo covered some of the main topics earlier in this thread:
https://lore.kernel.org/all/aZ3ejedS7nE5mnva@tpad/
But in syntax:
- There are workloads that are projected not avoid as much as possible return to kernelspace, as they are either cpu intensive, or latency sensitive (RT workloads) such as low-latency automation.There are scenarios such as industrial automation in which
the applications are supposed to reply a request in less than 50us since it was generated (IIRC), so sched-out, dealing with interruptions, or syscalls are a no-go. In those cases, using cpu isolation is a must, and since it can stay really long running in userspace, it may take a very long time to do any syscall to actually perform the scheduled flush.- Other workloads may need to use syscalls, or rely in interrupts, such as HPC, but it's also not interesting to take long on them, as the time spent there is time not used for processing the required data.
Let's say that for the sake of cpu isolation, a lot of different requests made to given isolated cpu are batched to be run on syscall entry/exit. It means the next syscall may take much longer than usual.
- This may break other RT workloads such as sensor/sound/image sampling, which could be generally ok with some of the faster syscalls for their application, and now may perceive an error because one of those syscalls took too long.While the qpw approach may cost a few extra cycles, it operates remotelly and makes the system a bit more predictable.
Quoted 6 lines
Also, when I was planning the mechanism, I remember it was meant to add zero overhead in case of
CONFIG_QPW=n, very little overhead in case ofCONFIG_QPW=y+ qpw=0 (a couple of static branches, possibly with the cost removed by the cpu branch predictor), and only add a few cycles in case of qpw=1 + !RT. Which means we may be missing just a few adjustments to get there.Quoted 6 lines
Leo,
v2 of the patchset adds only 2 cycles to
CONFIG_QPW=y+ qpw=0. The larger overhead was due tomigrate_disable, which is now (on v2) hidden inside the static branch.
My bad.Hi Marcelo,
Quoted 7 lines
Great, hiding
migrate_disableunder the static branch is the best scenario.I wonder why we spend 2 cycles on the static branches, though, should be close to nothing unless the branch predictor is too busy already. Well, we can always try to optimize in a different way.
Thanks for the effort on this!
Leo,
migrate_enable was leaking out of the static key section into the common error path.
With preempt_disable, as suggested by Vlastimil, those 2 cycles are gone:
[ 61.217232] kmalloc_bench: Avg cycles per kmalloc: 164 [ 68.047789] kmalloc_bench: Avg cycles per kmalloc: 165 [ 73.266568] kmalloc_bench: Avg cycles per kmalloc: 165 [ 120.634168] kmalloc_bench: Avg cycles per kmalloc: 164 [ 127.617872] kmalloc_bench: Avg cycles per kmalloc: 164 [ 157.803679] kmalloc_bench: Avg cycles per kmalloc: 163 [root@fedvm kmalloc-perf-test]# dmesg | grep qpw [ 0.000000] Command line: BOOT_IMAGE=(hd0,gpt2)/vmlinuz-tip root=UUID=35cfa00b-ed70-483f-b7b2-1964e14f719e ro rootflags=subvol=root console=ttyS0,115200 qpw=0 skew_tick=1 tsc=reliable rcupdate.rcu_normal_after_boot=1 rcutree.nohz_full_patience_delay=1000 isolcpus=managed_irq,domain,14,15 amd_pstate=disable nosoftlockup crashkernel=1024M [ 0.118274] Kernel command line: BOOT_IMAGE=(hd0,gpt2)/vmlinuz-tip root=UUID=35cfa00b-ed70-483f-b7b2-1964e14f719e ro rootflags=subvol=root console=ttyS0,115200 qpw=0 skew_tick=1 tsc=reliable rcupdate.rcu_normal_after_boot=1 rcutree.nohz_full_patience_delay=1000 isolcpus=managed_irq,domain,14,15 amd_pstate=disable nosoftlockup crashkernel=1024M
Quoted 158 lines · Marcelo Tosatti
Quoted 141 lines · Leonardo Bras
Quoted 131 lines · Marcelo Tosatti
Quoted 123 lines · Leonardo Bras
Quoted 78 lines · Michal Hocko
Quoted 77 lines · Leonardo Bras
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 4 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big.
Awesome! Thanks for reviewing!
Quoted 5 lines
To me, a much
more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.Quoted 7 lines
Quoted 5 lines
That crossed my mind as well, and I went with the idea of changing locking because I was working on workloads in which deferring work to a kernel re-entry would cause deadline misses as well. Or more critically, the drains could take forever, as some of those tasks would avoid returning to kernel as much as possible.
Could you be more specific please?
Hi Michal,
Sorry for the delayI think Marcelo covered some of the main topics earlier in this thread:
https://lore.kernel.org/all/aZ3ejedS7nE5mnva@tpad/
But in syntax:
- There are workloads that are projected not avoid as much as possible return to kernelspace, as they are either cpu intensive, or latency sensitive (RT workloads) such as low-latency automation.There are scenarios such as industrial automation in which
the applications are supposed to reply a request in less than 50us since it was generated (IIRC), so sched-out, dealing with interruptions, or syscalls are a no-go. In those cases, using cpu isolation is a must, and since it can stay really long running in userspace, it may take a very long time to do any syscall to actually perform the scheduled flush.- Other workloads may need to use syscalls, or rely in interrupts, such as HPC, but it's also not interesting to take long on them, as the time spent there is time not used for processing the required data.
Let's say that for the sake of cpu isolation, a lot of different requests made to given isolated cpu are batched to be run on syscall entry/exit. It means the next syscall may take much longer than usual.
- This may break other RT workloads such as sensor/sound/image sampling, which could be generally ok with some of the faster syscalls for their application, and now may perceive an error because one of those syscalls took too long.While the qpw approach may cost a few extra cycles, it operates remotelly and makes the system a bit more predictable.
Quoted 6 lines
Also, when I was planning the mechanism, I remember it was meant to add zero overhead in case of
CONFIG_QPW=n, very little overhead in case ofCONFIG_QPW=y+ qpw=0 (a couple of static branches, possibly with the cost removed by the cpu branch predictor), and only add a few cycles in case of qpw=1 + !RT. Which means we may be missing just a few adjustments to get there.Quoted 6 lines
Leo,
v2 of the patchset adds only 2 cycles to
CONFIG_QPW=y+ qpw=0. The larger overhead was due tomigrate_disable, which is now (on v2) hidden inside the static branch.
My bad.Hi Marcelo,
Quoted 7 lines
Great, hiding
migrate_disableunder the static branch is the best scenario.I wonder why we spend 2 cycles on the static branches, though, should be close to nothing unless the branch predictor is too busy already. Well, we can always try to optimize in a different way.
Thanks for the effort on this!
Leo,
migrate_enablewas leaking out of the static key section into the common error path.With
preempt_disable, as suggested by Vlastimil, those 2 cycles are gone:
Quoted 10 lines
[ 61.217232] kmalloc_bench: Avg cycles per kmalloc: 164 [ 68.047789] kmalloc_bench: Avg cycles per kmalloc: 165 [ 73.266568] kmalloc_bench: Avg cycles per kmalloc: 165 [ 120.634168] kmalloc_bench: Avg cycles per kmalloc: 164 [ 127.617872] kmalloc_bench: Avg cycles per kmalloc: 164 [ 157.803679] kmalloc_bench: Avg cycles per kmalloc: 163 [root@fedvm kmalloc-perf-test]# dmesg | grep qpw [ 0.000000] Command line: BOOT_IMAGE=(hd0,gpt2)/vmlinuz-tip root=UUID=35cfa00b-ed70-483f-b7b2-1964e14f719e ro rootflags=subvol=root console=ttyS0,115200 qpw=0 skew_tick=1 tsc=reliable rcupdate.rcu_normal_after_boot=1 rcutree.nohz_full_patience_delay=1000 isolcpus=managed_irq,domain,14,15 amd_pstate=disable nosoftlockup crashkernel=1024M [ 0.118274] Kernel command line: BOOT_IMAGE=(hd0,gpt2)/vmlinuz-tip root=UUID=35cfa00b-ed70-483f-b7b2-1964e14f719e ro rootflags=subvol=root console=ttyS0,115200 qpw=0 skew_tick=1 tsc=reliable rcupdate.rcu_normal_after_boot=1 rcutree.nohz_full_patience_delay=1000 isolcpus=managed_irq,domain,14,15 amd_pstate=disable nosoftlockup crashkernel=1024M
Ohh, awesome then.
That means we can have a QPW-enabled kernel and zero overhead perceived if qpw=0, right?
(Yeah, instruction cache will have to fetch extra instructions, but hopefully that stays hidden enough.)
Thanks!
Leo
Quoted 170 lines · Leonardo Bras
Quoted 158 lines · Marcelo Tosatti
Quoted 141 lines · Leonardo Bras
Quoted 131 lines · Marcelo Tosatti
Quoted 123 lines · Leonardo Bras
Quoted 78 lines · Michal Hocko
Quoted 77 lines · Leonardo Bras
Quoted 63 lines · Michal Hocko
Quoted 62 lines · Leonardo Bras
Quoted 31 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
Quoted 6 lines
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?Hi Michal,
I have done some study on this (which I presented on Plumbers 2023):
https://lpc.events/event/17/contributions/1484/Since they are per-cpu spinlocks, and the remote operations are not that frequent, as per design of the current approach, we are not supposed to see contention (I was not able to detect contention even after stress testing for weeks), nor relevant cacheline bouncing.
That being said, for RT
local_locksalready get per-cpu spinlocks, so there is only difference for !RT, which as you mention, doespreemtp_disable():The performance impact noticed was mostly about jumping around in executable code, as inlining spinlocks (test #2 on presentation) took care of most of the added extra cycles, adding about 4-14 extra cycles per lock/unlock cycle. (tested on memcg with kmalloc test)
Yeah, as expected there is some extra cycles, as we are doing extra atomic operations (even if in a local cacheline) in !RT case, but this could be enabled only if the user thinks this is an ok cost for reducing interruptions.
Quoted 4 lines
Quoted 1 line
What do you think?
The fact that the behavior is opt-in for !RT is certainly a plus. I also do not expect the overhead to be really be really big.
Awesome! Thanks for reviewing!
Quoted 5 lines
To me, a much
more important question is which of the two approaches is easier to maintain long term. The pcp work needs to be done one way or the other. Whether we want to tweak locking or do it at a very well defined time is the bigger question.Quoted 7 lines
Quoted 5 lines
That crossed my mind as well, and I went with the idea of changing locking because I was working on workloads in which deferring work to a kernel re-entry would cause deadline misses as well. Or more critically, the drains could take forever, as some of those tasks would avoid returning to kernel as much as possible.
Could you be more specific please?
Hi Michal,
Sorry for the delayI think Marcelo covered some of the main topics earlier in this thread:
https://lore.kernel.org/all/aZ3ejedS7nE5mnva@tpad/
But in syntax:
- There are workloads that are projected not avoid as much as possible return to kernelspace, as they are either cpu intensive, or latency sensitive (RT workloads) such as low-latency automation.There are scenarios such as industrial automation in which
the applications are supposed to reply a request in less than 50us since it was generated (IIRC), so sched-out, dealing with interruptions, or syscalls are a no-go. In those cases, using cpu isolation is a must, and since it can stay really long running in userspace, it may take a very long time to do any syscall to actually perform the scheduled flush.- Other workloads may need to use syscalls, or rely in interrupts, such as HPC, but it's also not interesting to take long on them, as the time spent there is time not used for processing the required data.
Let's say that for the sake of cpu isolation, a lot of different requests made to given isolated cpu are batched to be run on syscall entry/exit. It means the next syscall may take much longer than usual.
- This may break other RT workloads such as sensor/sound/image sampling, which could be generally ok with some of the faster syscalls for their application, and now may perceive an error because one of those syscalls took too long.While the qpw approach may cost a few extra cycles, it operates remotelly and makes the system a bit more predictable.
Quoted 6 lines
Also, when I was planning the mechanism, I remember it was meant to add zero overhead in case of
CONFIG_QPW=n, very little overhead in case ofCONFIG_QPW=y+ qpw=0 (a couple of static branches, possibly with the cost removed by the cpu branch predictor), and only add a few cycles in case of qpw=1 + !RT. Which means we may be missing just a few adjustments to get there.Quoted 6 lines
Leo,
v2 of the patchset adds only 2 cycles to
CONFIG_QPW=y+ qpw=0. The larger overhead was due tomigrate_disable, which is now (on v2) hidden inside the static branch.
My bad.Hi Marcelo,
Quoted 7 lines
Great, hiding
migrate_disableunder the static branch is the best scenario.I wonder why we spend 2 cycles on the static branches, though, should be close to nothing unless the branch predictor is too busy already. Well, we can always try to optimize in a different way.
Thanks for the effort on this!
Leo,
migrate_enablewas leaking out of the static key section into the common error path.With
preempt_disable, as suggested by Vlastimil, those 2 cycles are gone:Quoted 10 lines
[ 61.217232] kmalloc_bench: Avg cycles per kmalloc: 164 [ 68.047789] kmalloc_bench: Avg cycles per kmalloc: 165 [ 73.266568] kmalloc_bench: Avg cycles per kmalloc: 165 [ 120.634168] kmalloc_bench: Avg cycles per kmalloc: 164 [ 127.617872] kmalloc_bench: Avg cycles per kmalloc: 164 [ 157.803679] kmalloc_bench: Avg cycles per kmalloc: 163 [root@fedvm kmalloc-perf-test]# dmesg | grep qpw [ 0.000000] Command line: BOOT_IMAGE=(hd0,gpt2)/vmlinuz-tip root=UUID=35cfa00b-ed70-483f-b7b2-1964e14f719e ro rootflags=subvol=root console=ttyS0,115200 qpw=0 skew_tick=1 tsc=reliable rcupdate.rcu_normal_after_boot=1 rcutree.nohz_full_patience_delay=1000 isolcpus=managed_irq,domain,14,15 amd_pstate=disable nosoftlockup crashkernel=1024M [ 0.118274] Kernel command line: BOOT_IMAGE=(hd0,gpt2)/vmlinuz-tip root=UUID=35cfa00b-ed70-483f-b7b2-1964e14f719e ro rootflags=subvol=root console=ttyS0,115200 qpw=0 skew_tick=1 tsc=reliable rcupdate.rcu_normal_after_boot=1 rcutree.nohz_full_patience_delay=1000 isolcpus=managed_irq,domain,14,15 amd_pstate=disable nosoftlockup crashkernel=1024M
Quoted 3 lines
Ohh, awesome then.
That means we can have a QPW-enabled kernel and zero overhead perceived if qpw=0, right?
Yes.
Quoted 2 lines
(Yeah, instruction cache will have to fetch extra instructions, but hopefully that stays hidden enough.)
Quoted 27 lines · Michal Hocko
Quoted 1 line · Marcelo Tosatti
On Tue, Feb 10, 2026 at 03:01:10PM +0100, Michal Hocko wrote:
[...]
Quoted 8 lines
Quoted 7 lines
What about !PREEMPT_RT? We have people running isolated workloads and these sorts of pcp disruptions are really unwelcome as well. They do not have requirements as strong as RT workloads but the underlying fundamental problem is the same. Frederic (now CCed) is working on moving those pcp book keeping activities to be executed to the return to the userspace which should be taking care of both RT and non-RT configurations AFAICS.
Quoted 5 lines
Michal,
For !PREEMPT_RT, _if_ you select
CONFIG_QPW=y, then there is a kernel boot option qpw=y/n, which controls whether the behaviour will be similar (the spinlock is taken onlocal_lock, similar toPREEMPT_RT).My bad. I've misread the config space of this.
Quoted 7 lines
If
CONFIG_QPW=n, or kernel boot option qpw=n, then onlylocal_lock(and remote work viawork_queue) is used.What "pcp book keeping activities" you refer to ? I don't see how moving certain activities that happen under SLUB or LRU spinlocks to happen before return to userspace changes things related to avoidance of CPU interruption ?
Quoted 7 lines
Essentially delayed operations like pcp state flushing happens on return to the userspace on isolated CPUs. No locking changes are required as the work is still per-cpu.
In other words the approach Frederic is working on is to not change the locking of pcp delayed work but instead move that work into well defined place - i.e. return to the userspace.
Michal,
I can't find such work from Frederic. Do you mean:
Subject: Re: [PATCH v6 00/29] context_tracking,x86: Defer some IPIs until a user->kernel transition
From Valentin ?
Or if you have a pointer to Frederic's work.
Quoted 2 lines
Btw. have you measure the impact of
preempt_disbale-> spinlock on hot paths like SLUB sheeves?
Doing that, will post results as soon as possible.