Re: [PATCH v2 13/20] arm64: percpu: Add infrastructure for preemptible this_cpu_*() ops
Pedro Falcato <[email protected]> Tue, 4 Aug 2026 23:45:56 +0100
| Newsgroups | gmane.linux.kernel.stable,gmane.linux.ports.arm.kernel |
|---|---|
| Message-ID | <[email protected]> |
On Tue, Aug 04, 2026 at 06:04:56PM +0100, Mark Rutland wrote:
> Currently arm64's this_cpu_*() ops transiently disable preemption in
> order to guarantee that the address generation and memory access(es)
> occur on the same CPU.
>
> Transiently disabling preemption can be expensive. When re-enabling
> preemption it is necessary to make a conditional function call to
> preempt_schedule[_notrace]() in order to handle the rare case that the
> task needs to be rescheduled. The potential function call has a number
> of negative effects on code generation (e.g. due to the need to create a
> stack frame and spill registers), and the conditionality can result in
> poor code generation and/or poor branch prediction.
>
> This patch adds infrastructure for a scheme where this_cpu_*() ops do
> not need to transiently disable preemption, avoiding the negative
> impacts described above. Individual operations will be converted in
> subsequent patches.
>
> Each operation registers a critical section during which the exception
> return code will adjust the offset and addresses if preemption occurs
> mid-sequence. The critical section is registered/unregistered with a
> small prologue and epilogue which encodes three distinct GPRRs (<pcp>,
> <off>, <addr>) into a new thread_info::pcp_gprs field:
>
> // Prologue. Enable fixups for <off> and <addr>.
> mrs <tsk>, sp_el0
> mov <tmp>, #__VAL_PCPU_GPRS(<pcp>, <off>, <addr>)
> strh <tmp>, [<tsk>, #TSK_TI_PCPU_GPRS]
>
> // Generate cpu-specific address
> mrs <off>, TPIDR_ELx
> add <addr>, <pcp>, <off>
>
> // Perform access sequence
> ldr <val>, [<addr>]
>
> // Epilogue. Disable fixups
> strh wzr, [<tsk>, #TSK_TI_PCPU_GPRS]
>
> If an exception is taken from within the critical section, the exception
> return code will adjust <off> to be the current CPU's offset, and will
> adjust <addr> to be (<pcp> + <off>). Distinct registers are used for
> <pcp>, <off>, and <addr>, so that the fixup can be applied safely at any
> point during the critical section.
>
> To ensure that this_cpu_*() operations within exception handlers work
> correctly and do not corrupt state, thread_info::pcpu_gprs is saved
> into a new pt_regs::pcpu_gprs field upon exception entry, and restored
> upon exception return.
>
> Looking at a simple this_cpu_operation:
>
> | void outline_this_cpu_add_u64(u64 __percpu *p, u64 v)
> | {
> | this_cpu_add(*p, v);
> | }
>
> Atop v7.2-rc4, with GCC 15.2.0 and defconfig, this is compiled as:
>
> | <outline_this_cpu_add_u64>:
> | paciasp
> | stp x29, x30, [sp, #-16]!
> | mrs x2, sp_el0
> | mov x29, sp
> | ldr w3, [x2, #8]
> | add w3, w3, #0x1
> | str w3, [x2, #8]
> | mrs x3, tpidr_el1
> | add x0, x0, x3
> | 1: ldxr x5, [x0]
> | add x5, x5, x1
> | stxr w4, x5, [x0]
> | cbnz w4, 1b
> | ldr x0, [x2, #8]
> | sub x0, x0, #0x1
> | str w0, [x2, #8]
> | cbz x0, 2f
> | ldr x0, [x2, #8]
> | cbnz x0, 3f
> | 2: bl preempt_schedule_notrace
> | 3: ldp x29, x30, [sp], #16
> | autiasp
> | ret
>
> With the scheme added in this patch, this can be compiled as:
>
> | <outline_this_cpu_add_u64>:
> | mrs x2, sp_el0
> | mov x4, #0xc80 // __VAL_PCPU_GPRS(x0, x4, x3)
> | strh w4, [x2, #20]
> | mrs x4, tpidr_el1
> | add x3, x0, x4
> | 1: ldxr x6, [x3]
> | add x6, x6, x1
> | stxr w5, x6, [x3]
> | cbnz w5, 1b
> | strh wzr, [x2, #20]
> | ret
I think I had an Interesting Idea(tm) while reading the per-cpu discussion
in linux-mm. In case the 3 instruction preamble is too expensive:
1) Pass -ffixed-x18 (this natively conflicts with SHADOW_CALL_STACK.
SHADOW_CALL_STACK is already not-optimal codegen wise, so maybe not a big deal).
2) arm64 kernel bits will use x18 as a cheap task flags register
3) #define TASK_KRSEQ (1 << 0)
4) Switching into the krseq mode is just a matter of toggling the bit in x18, so
orr x18, x18, #TASK_KRSEQ
a single instruction.
5) Switching off is just a matter of clearing the bit in x18, so:
and x18, x18, #~TASK_KRSEQ
6) On the preempt side we keep the krseq tables in memory, and do a sort of lookup
(binary search sounds easiest?) on them. But _only_ if x18 TASK_KRSEQ is set.
This penalises unlucky preempts but keeps fast paths maximally fast.
7) entry points of course get to clear it after saving it
The end result would look something like:
| <outline_this_cpu_add_u64>:
| orr x18, x18, #TASK_KRSEQ
| mrs x4, tpidr_el1
| add x3, x0, x4
| 1: ldxr x6, [x3]
| add x6, x6, x1
| stxr w5, x6, [x3]
| cbnz w5, 1b
| 2:
| and x18, x18, #~TASK_KRSEQ
| ret
| .pushsection .data.krseq
| .word 1b
| .word 2b
| .word whateverelse
| .popsection
This of course precludes the use of x18 for the compiler, so it would
require careful benchmarking in case it negatively affects codegen too much.
But it avoids any sort of extraneous stores in the fast path.
Other architectures could do similar as long as they have interesting ways of
signaling this using solely the register set.
Anyway, just throwing it out there in case this can actually make a
difference & rings some bells on people smarter than me :)
--
Pedro