Skip to content

Arm64: opportunistically light up LSE atomics for NativeAOT - #132358

Draft
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:arm64-lse-lightup-naot
Draft

Arm64: opportunistically light up LSE atomics for NativeAOT#132358
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:arm64-lse-lightup-naot

Conversation

@EgorBo

@EgorBo EgorBo commented Aug 15, 2026

Copy link
Copy Markdown
Member

Closes #129951

.NET 12.0

Using an internal Lse.cs intrinsic API with IsSupported and 5 generic APIs (they emit a call to NotSupportedException when called for unsupported T).

NativeAOT codegen, linux-arm64 (LSE is not in the baseline there, so it is a runtime check)
[MethodImpl(MethodImplOptions.NoInlining)]
public static int Add(ref int a, int b) => Interlocked.Add(ref a, b);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w3, [x2]
            tbnz    w3, #6, IG09
IG03:
            ldr     w3, [x0]
            add     w4, w3, w1
            ldr     w5, [x2]
            tbnz    w5, #6, IG06
IG04:
            ldaxr   w6, [x0]
            cmp     w6, w3
            bne     IG05
            stlxr   w5, w4, [x0]
            cbnz    w5, IG04
IG05:
            dmb     ish
            b       IG07
IG06:
            mov     w6, w3
            casal   w6, w4, [x0]
IG07:
            cmp     w6, w3
            bne     IG03
IG08:
            b       IG10
IG09:
            ldaddal w1, w3, [x0]
IG10:
            add     w0, w3, w1
IG11:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Add_void(ref int a, int b) => Interlocked.Add(ref a, b);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w3, [x2]
            tbnz    w3, #6, IG09
IG03:
            ldr     w3, [x0]
            add     w4, w3, w1
            ldr     w5, [x2]
            tbnz    w5, #6, IG06
IG04:
            ldaxr   w6, [x0]
            cmp     w6, w3
            bne     IG05
            stlxr   w5, w4, [x0]
            cbnz    w5, IG04
IG05:
            dmb     ish
            b       IG07
IG06:
            mov     w6, w3
            casal   w6, w4, [x0]
IG07:
            cmp     w6, w3
            bne     IG03
IG08:
            ldp     fp, lr, [sp], #0x10
            ret     lr
IG09:
            ldaddal w1, w0, [x0]
            b       IG08
[MethodImpl(MethodImplOptions.NoInlining)]
public static int Increment(ref int a) => Interlocked.Increment(ref a);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x1, [HIGH RELOC #0x422388]      // static handle
            add     x1, x1, [LOW RELOC #0x422388]
            ldr     w2, [x1]
            tbnz    w2, #6, IG09
IG03:
            ldr     w2, [x0]
            add     w3, w2, #1
            ldr     w4, [x1]
            tbnz    w4, #6, IG06
IG04:
            ldaxr   w5, [x0]
            cmp     w5, w2
            bne     IG05
            stlxr   w4, w3, [x0]
            cbnz    w4, IG04
IG05:
            dmb     ish
            b       IG07
IG06:
            mov     w5, w2
            casal   w5, w3, [x0]
IG07:
            cmp     w5, w2
            bne     IG03
IG08:
            b       IG10
IG09:
            mov     w2, #1
            ldaddal w2, w2, [x0]
IG10:
            add     w0, w2, #1
IG11:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Increment_void(ref int a) => Interlocked.Increment(ref a);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x1, [HIGH RELOC #0x422388]      // static handle
            add     x1, x1, [LOW RELOC #0x422388]
            ldr     w2, [x1]
            tbnz    w2, #6, IG09
IG03:
            ldr     w2, [x0]
            add     w3, w2, #1
            ldr     w4, [x1]
            tbnz    w4, #6, IG06
IG04:
            ldaxr   w5, [x0]
            cmp     w5, w2
            bne     IG05
            stlxr   w4, w3, [x0]
            cbnz    w4, IG04
IG05:
            dmb     ish
            b       IG07
IG06:
            mov     w5, w2
            casal   w5, w3, [x0]
IG07:
            cmp     w5, w2
            bne     IG03
IG08:
            ldp     fp, lr, [sp], #0x10
            ret     lr
IG09:
            mov     w1, #1
            ldaddal w1, w0, [x0]
            b       IG08
[MethodImpl(MethodImplOptions.NoInlining)]
public static int Decrement(ref int a) => Interlocked.Decrement(ref a);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x1, [HIGH RELOC #0x422388]      // static handle
            add     x1, x1, [LOW RELOC #0x422388]
            ldr     w2, [x1]
            tbnz    w2, #6, IG09
IG03:
            ldr     w2, [x0]
            sub     w3, w2, #1
            ldr     w4, [x1]
            tbnz    w4, #6, IG06
IG04:
            ldaxr   w5, [x0]
            cmp     w5, w2
            bne     IG05
            stlxr   w4, w3, [x0]
            cbnz    w4, IG04
IG05:
            dmb     ish
            b       IG07
IG06:
            mov     w5, w2
            casal   w5, w3, [x0]
IG07:
            cmp     w5, w2
            bne     IG03
IG08:
            b       IG10
IG09:
            movn    w2, #0
            ldaddal w2, w2, [x0]
IG10:
            sub     w0, w2, #1
IG11:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Decrement_void(ref int a) => Interlocked.Decrement(ref a);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x1, [HIGH RELOC #0x422388]      // static handle
            add     x1, x1, [LOW RELOC #0x422388]
            ldr     w2, [x1]
            tbnz    w2, #6, IG09
IG03:
            ldr     w2, [x0]
            sub     w3, w2, #1
            ldr     w4, [x1]
            tbnz    w4, #6, IG06
IG04:
            ldaxr   w5, [x0]
            cmp     w5, w2
            bne     IG05
            stlxr   w4, w3, [x0]
            cbnz    w4, IG04
IG05:
            dmb     ish
            b       IG07
IG06:
            mov     w5, w2
            casal   w5, w3, [x0]
IG07:
            cmp     w5, w2
            bne     IG03
IG08:
            ldp     fp, lr, [sp], #0x10
            ret     lr
IG09:
            movn    w1, #0
            ldaddal w1, w0, [x0]
            b       IG08
[MethodImpl(MethodImplOptions.NoInlining)]
public static int Exchange(ref int a, int b) => Interlocked.Exchange(ref a, b);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w2, [x2]
            tbnz    w2, #6, IG04
IG03:
            ldaxr   w3, [x0]
            stlxr   w2, w1, [x0]
            cbnz    w2, IG03
            dmb     ish
            b       IG05
IG04:
            swpal   w1, w3, [x0]
IG05:
            mov     w0, w3
IG06:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Exchange_void(ref int a, int b) => Interlocked.Exchange(ref a, b);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w2, [x2]
            tbnz    w2, #6, IG04
IG03:
            ldaxr   w3, [x0]
            stlxr   w2, w1, [x0]
            cbnz    w2, IG03
            dmb     ish
            b       IG05
IG04:
            swpal   w1, w0, [x0]
IG05:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static int CompareExchange(ref int a, int b, int c) => Interlocked.CompareExchange(ref a, b, c);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x3, [HIGH RELOC #0x422388]      // static handle
            add     x3, x3, [LOW RELOC #0x422388]
            ldr     w3, [x3]
            tbnz    w3, #6, IG05
IG03:
            ldaxr   w4, [x0]
            cmp     w4, w2
            bne     IG04
            stlxr   w3, w1, [x0]
            cbnz    w3, IG03
IG04:
            dmb     ish
            b       IG06
IG05:
            mov     w4, w2
            casal   w4, w1, [x0]
IG06:
            mov     w0, w4
IG07:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static void CompareExchange_void(ref int a, int b, int c) => Interlocked.CompareExchange(ref a, b, c);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x3, [HIGH RELOC #0x422388]      // static handle
            add     x3, x3, [LOW RELOC #0x422388]
            ldr     w3, [x3]
            tbnz    w3, #6, IG05
IG03:
            ldaxr   w4, [x0]
            cmp     w4, w2
            bne     IG04
            stlxr   w3, w1, [x0]
            cbnz    w3, IG03
IG04:
            dmb     ish
            b       IG06
IG05:
            casal   w2, w1, [x0]
IG06:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static int And(ref int a, int b) => Interlocked.And(ref a, b);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w3, [x2]
            tbnz    w3, #6, IG11
IG03:
            ldr     w3, [x0]
            b       IG05
IG04:
            mov     w3, w6
IG05:
            and     w4, w3, w1
            ldr     w5, [x2]
            tbnz    w5, #6, IG08
IG06:
            ldaxr   w6, [x0]
            cmp     w6, w3
            bne     IG07
            stlxr   w5, w4, [x0]
            cbnz    w5, IG06
IG07:
            dmb     ish
            b       IG09
IG08:
            mov     w6, w3
            casal   w6, w4, [x0]
IG09:
            cmp     w6, w3
            bne     IG04
IG10:
            b       IG12
IG11:
            mvn     w3, w1
            ldclral w3, w3, [x0]
IG12:
            mov     w0, w3
IG13:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static void And_void(ref int a, int b) => Interlocked.And(ref a, b);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w3, [x2]
            tbnz    w3, #6, IG11
IG03:
            ldr     w3, [x0]
            b       IG05
IG04:
            mov     w3, w6
IG05:
            and     w4, w3, w1
            ldr     w5, [x2]
            tbnz    w5, #6, IG08
IG06:
            ldaxr   w6, [x0]
            cmp     w6, w3
            bne     IG07
            stlxr   w5, w4, [x0]
            cbnz    w5, IG06
IG07:
            dmb     ish
            b       IG09
IG08:
            mov     w6, w3
            casal   w6, w4, [x0]
IG09:
            cmp     w6, w3
            bne     IG04
IG10:
            ldp     fp, lr, [sp], #0x10
            ret     lr
IG11:
            mvn     w2, w1
            ldclral w2, w0, [x0]
            b       IG10
[MethodImpl(MethodImplOptions.NoInlining)]
public static int Or(ref int a, int b) => Interlocked.Or(ref a, b);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w3, [x2]
            tbnz    w3, #6, IG11
IG03:
            ldr     w3, [x0]
            b       IG05
IG04:
            mov     w3, w6
IG05:
            orr     w4, w3, w1
            ldr     w5, [x2]
            tbnz    w5, #6, IG08
IG06:
            ldaxr   w6, [x0]
            cmp     w6, w3
            bne     IG07
            stlxr   w5, w4, [x0]
            cbnz    w5, IG06
IG07:
            dmb     ish
            b       IG09
IG08:
            mov     w6, w3
            casal   w6, w4, [x0]
IG09:
            cmp     w6, w3
            bne     IG04
IG10:
            b       IG12
IG11:
            ldsetal w1, w3, [x0]
IG12:
            mov     w0, w3
IG13:
            ldp     fp, lr, [sp], #0x10
            ret     lr
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Or_void(ref int a, int b) => Interlocked.Or(ref a, b);
IG01:
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
IG02:
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w3, [x2]
            tbnz    w3, #6, IG11
IG03:
            ldr     w3, [x0]
            b       IG05
IG04:
            mov     w3, w6
IG05:
            orr     w4, w3, w1
            ldr     w5, [x2]
            tbnz    w5, #6, IG08
IG06:
            ldaxr   w6, [x0]
            cmp     w6, w3
            bne     IG07
            stlxr   w5, w4, [x0]
            cbnz    w5, IG06
IG07:
            dmb     ish
            b       IG09
IG08:
            mov     w6, w3
            casal   w6, w4, [x0]
IG09:
            cmp     w6, w3
            bne     IG04
IG10:
            ldp     fp, lr, [sp], #0x10
            ret     lr
IG11:
            ldsetal w1, w0, [x0]
            b       IG10

EgorBo and others added 3 commits August 15, 2026 13:04
NativeAOT's arm64 baseline is armv8-a, so `Interlocked` operations were stuck
emitting `ldaxr`/`stlxr` retry loops even though virtually all real hardware
implements the Armv8.1 LSE atomics. The runtime JIT already picks the single
instruction forms because it knows the exact CPU it is running on; AOT could not,
because the check has to happen at runtime.

Rather than teach codegen to branch on a CPU feature word, the check is expressed
in managed code. A new internal `System.Runtime.Intrinsics.Arm.Lse` hardware
intrinsic class exposes the five atomic primitives, and the `Interlocked` bodies
call them behind `if (Lse.IsSupported)`. On NativeAOT `IsSupported` already lowers
to a cached CPU feature query, so the existing opportunistic light-up machinery
does all the work and no new JIT/EE API is needed.

To make that check observable, the importer declines to intrinsify `Interlocked`
exactly when the atomics are opportunistically available but not part of the
baseline - the "Dynamic" state, which in practice only NativeAOT ever reaches.
The call is then imported normally, the body is inlined, and the check lights up.
Recursion is bounded by `mustExpand`: the fallback arm calls the same API from
within its own body, and recursive intrinsic calls must expand, so it always
bottoms out in the ldaxr/stlxr sequence.

Every other configuration keeps the state it had before - baseline LSE stays
baseline, no-LSE stays no-LSE - so this is a strict no-op for the runtime JIT,
R2R, and NativeAOT on Windows/macOS. SuperPMI asmdiffs over the linux-arm64
benchmarks.run collection report zero diffs across 142,660 contexts.

Covers CompareExchange, Exchange, ExchangeAdd (Add/Increment/Decrement), And,
and Or, for both int and long.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec0dae32-314b-42fd-a971-95921ea8ebb4
- Add the "outlined on AOT" comment to the other two Interlocked files; it was
  only in Interlocked.CoreCLR.cs.
- Promote the two base type checks in the Lse importer to noway_assert. These
  APIs are generic, so an unsupported type argument would otherwise silently
  produce an atomic access of the wrong width in a release JIT; now it fails the
  compilation instead. Verified: Lse.LoadAdd<float> fails importation rather
  than emitting a bogus ldaddal, and Lse.Swap<DayOfWeek> correctly emits a
  32-bit swpal (the JIT sees enums as their underlying primitive).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec0dae32-314b-42fd-a971-95921ea8ebb4
Replace the noway_asserts guarding the Lse type argument with the standard
impUnsupportedNamedIntrinsic path, so an unsupported instantiation compiles into
a throw helper call rather than killing the compilation.

The check is made before any argument is popped, and asks for a throwing
expansion unconditionally because the managed bodies are recursive stubs -
returning nullptr would fall back to a real call that recurses forever.

Verified on arm64: Lse.LoadAdd<float> now emits a call to
CORINFO_HELP_THROW_PLATFORM_NOT_SUPPORTED (AOT remaps TypeNotSupported to PNSE)
instead of tripping an assert, and Lse.Swap<DayOfWeek> still emits swpal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec0dae32-314b-42fd-a971-95921ea8ebb4
Copilot AI lite review requested due to automatic review settings August 15, 2026 12:48
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Aug 15, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 8 pipeline(s).
8 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Validated the real ILC path for the first time (x64-hosted ILC, --targetarch
arm64 --targetos linux) rather than the JitStressAtomicsLightUp knob, and found
that Exchange(ref int), Exchange(ref long) and CompareExchange(ref int) were
left as out-of-line calls:

    bl      System.Threading.Interlocked:Exchange(byref,int):int

Those three are the only ones of the group that were not already marked
AggressiveInlining. It never mattered before because they were always
intrinsified at the call site, but now that the JIT declines to intrinsify them
the inliner has to carry them, and its cost model rejects the larger body.

With the attribute they inline as intended on linux-arm64:

    ldr     w2, [x2]                ; cached CPU features
    tbnz    w2, dotnet#6, LSE             ; bit 6 == Arm64IntrinsicConstants.Atomics
    ldaxr/stlxr ...                 ; armv8-a fallback
    LSE: swpal w0, w3, [x1]

win-arm64 is unchanged (LSE is in its baseline): no feature check and no
ldaxr/stlxr anywhere, just casal/swpal/ldaddal/ldsetal/ldclral.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec0dae32-314b-42fd-a971-95921ea8ebb4

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opportunistic Arm64 LSE (Armv8.1 atomics) fast path for System.Threading.Interlocked by introducing an internal System.Runtime.Intrinsics.Arm.Lse intrinsic and updating CoreCLR JIT lowering/LSRA/codegen to correctly select LSE vs ldaxr/stlxr expansion per-node (including a “dynamic/light-up” mode used by NativeAOT).

Changes:

  • Add internal System.Runtime.Intrinsics.Arm.Lse intrinsic API and wire it into the CoreLib build and the instruction-set metadata plumbing.
  • Update Interlocked implementations (CoreLib/CoreCLR/NativeAOT) to use if (Lse.IsSupported) to select LSE atomics, falling back to existing loops/intrinsics otherwise.
  • Teach the Arm64 JIT to represent/route LSE atomics via GTF_ATOMIC_LSE and AtomicsImpl selection, ensuring correct containment decisions, LSRA behavior, CSE behavior, and codegen emission.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/libraries/System.Private.CoreLib/src/System/Threading/Interlocked.cs Add Arm64 Lse.IsSupported fast paths for And/Or to use LSE atomics when available.
src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Arm/Lse.cs New internal intrinsic surface for Arm64 LSE atomics used by Interlocked.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems Include the new Lse.cs in the CoreLib build.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/InstructionSetDesc.txt Map ARM64 Atomics (“lse”) to managed type name Lse.
src/coreclr/tools/Common/JitInterface/CorInfoInstructionSet.cs Teach tooling to recognize Lse as the ARM64 Atomics instruction set and enumerate the type.
src/coreclr/System.Private.CoreLib/src/System/Threading/Interlocked.CoreCLR.cs Add Lse.IsSupported fast paths for Exchange/CompareExchange/ExchangeAdd on Arm64.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Interlocked.cs Add Lse.IsSupported fast paths for NativeAOT Interlocked intrinsics on Arm64.
src/coreclr/jit/lsraarm64.cpp Adjust internal-reg/lifetime decisions based on whether a node will lower to LSE vs LL/SC.
src/coreclr/jit/lowerarmarch.cpp Prevent immediate containment for atomic ops when the chosen expansion is LSE.
src/coreclr/jit/jitconfigvalues.h Add JitStressAtomicsLightUp to force exercising the managed light-up path under the runtime JIT.
src/coreclr/jit/importercalls.cpp Add logic to selectively not expand certain Interlocked intrinsics to enable managed light-up in “dynamic” mode.
src/coreclr/jit/hwintrinsiclistarm64.h Define new Atomics (LSE) hardware intrinsic entries and instruction mappings.
src/coreclr/jit/hwintrinsicarm64.cpp Recognize Lse ISA, import Atomics intrinsics to existing atomic nodes marked GTF_ATOMIC_LSE.
src/coreclr/jit/hwintrinsic.cpp Register the Atomics intrinsic range so it’s discoverable as an ISA group.
src/coreclr/jit/gentree.h Add GTF_ATOMIC_LSE flag to distinguish LSE atomic nodes.
src/coreclr/jit/gentree.cpp Prevent CSE merging of atomic nodes that differ only by GTF_ATOMIC_LSE.
src/coreclr/jit/compiler.h Introduce AtomicsImpl and compGetAtomicsImplForNode to select expansion strategy per node.
src/coreclr/jit/compiler.cpp Implement cached compGetAtomicsImpl() with dynamic/light-up selection and stress support.
src/coreclr/jit/codegenarm64.cpp Emit LSE single-instruction atomics when requested; otherwise emit LL/SC sequences.

Comment on lines +18 to +21
/// Unlike the Interlocked APIs these never fall back to an ldaxr/stlxr retry loop, so they may
/// only be called when <see cref="IsSupported"/> is true. The type argument must be an integer
/// (or an enum over one) no wider than a pointer; sizes of 1 and 2 bytes are only supported by
/// <see cref="CompareAndSwap{T}"/> and <see cref="Swap{T}"/>. Anything else fails to compile.
Comment on lines +871 to +877
#if defined(TARGET_ARM64)
// Stress the opportunistic light-up of the Armv8.1 LSE atomics: pretend they are not part of the
// baseline instruction set even when they are, so that the managed `if (Lse.IsSupported)` checks in
// the Interlocked APIs are used. This state occurs naturally for NativeAOT, but never for the
// runtime JIT, so this is the only way to cover it outside of AOT compilation.
CONFIG_INTEGER(JitStressAtomicsLightUp, "JitStressAtomicsLightUp", 0)
#endif // TARGET_ARM64
Copilot AI review requested due to automatic review settings August 15, 2026 12:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Arm/Lse.cs:21

  • The remark "Anything else fails to compile" is misleading: unsupported type arguments are rejected by the JIT/importer by throwing, rather than being a C# compile-time error. Please reword this to describe the runtime behavior accurately.
    /// Unlike the Interlocked APIs these never fall back to an ldaxr/stlxr retry loop, so they may
    /// only be called when <see cref="IsSupported"/> is true. The type argument must be an integer
    /// (or an enum over one) no wider than a pointer; sizes of 1 and 2 bytes are only supported by
    /// <see cref="CompareAndSwap{T}"/> and <see cref="Swap{T}"/>. Anything else fails to compile.

@EgorBo

EgorBo commented Aug 15, 2026

Copy link
Copy Markdown
Member Author
            adrp    x2, [HIGH RELOC #0x422388]      // static handle
            add     x2, x2, [LOW RELOC #0x422388]
            ldr     w3, [x2]
            tbnz    w3, #6, IG09

ISA check is not cheap on arm-aot 😞 (still should be a massive win under high contention)

crossgen-corelib for maccatalyst-arm64 failed:

  Code generation failed for method
  '[S.P.CoreLib]System.Runtime.Intrinsics.Arm.Lse.CompareAndSwap<T>(!!0&,!!0,!!0)'
  ---> System.InvalidCastException: Unable to cast object of type
       'Internal.TypeSystem.SignatureMethodVariable' to type 'Internal.TypeSystem.DefType'

R2R compiles the uninstantiated definition of a generic method. The usual
recursive intrinsic stub then presents the importer with a self-call whose type
argument is still a signature method variable, and trying to intrinsify that
falls over. Apple arm64 is where it shows up because its baseline is apple-m1,
so Lse.IsSupported folds to true and the call is live; everywhere else the ISA
is not in the baseline, IsSupported folds to false and the body is dead code.

The recursive stub is a convention for non-generic intrinsics and buys nothing
here, so throw instead. All call sites are expanded at the call site, and a
caller that ignores the contract now gets an exception rather than unbounded
recursion.

Verified by reproducing the exact crossgen2 command line locally (composite,
mibc, macho, --targetos:maccatalyst) before and after. NativeAOT linux-arm64
light-up is unchanged (tbnz dotnet#6 plus swpal/casal/ldaddal/ldsetal/ldclral, no
out-of-line calls), maccatalyst still emits baseline LSE with no feature checks,
SPMI asmdiffs remain at 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec0dae32-314b-42fd-a971-95921ea8ebb4
Copilot AI review requested due to automatic review settings August 15, 2026 13:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Arm/Lse.cs:21

  • The remarks say unsupported type arguments/sizes “fail to compile”, but the JIT path for these intrinsics treats unsupported types as a runtime throw (see impUnsupportedNamedIntrinsic(CORINFO_HELP_THROW_TYPE_NOT_SUPPORTED) in the special-import path). The docs should match the actual behavior so callers don’t assume compile-time failure.
    /// Unlike the Interlocked APIs these never fall back to an ldaxr/stlxr retry loop, so they may
    /// only be called when <see cref="IsSupported"/> is true. The type argument must be an integer
    /// (or an enum over one) no wider than a pointer; sizes of 1 and 2 bytes are only supported by
    /// <see cref="CompareAndSwap{T}"/> and <see cref="Swap{T}"/>. Anything else fails to compile.

@teo-tsirpanis teo-tsirpanis added this to the 12.0.0 milestone Aug 15, 2026
public static new bool IsSupported { get => IsSupported; }

// Note these are deliberately not the usual recursive intrinsic stubs. They are generic, and
// R2R compiles the uninstantiated definition of a generic method, which would then try to

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uninstantiated method definitions cannot be ever compiled. This comment does not make sense.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All HW intrinsics aren't generic, I wonder if I should make LSE non-generic too, but then it will introduce a big type-switch (with unsafe bitcast to handle enums) 🤔 or keep it as is.

Reverts the previous commit's workaround and fixes the actual problem, which
jkotas correctly pointed out my comment had described nonsensically: an
uninstantiated generic method definition is never supposed to be compiled.

ReadyToRunHardwareIntrinsicRootProvider roots every method on an intrinsic class
whose ISA is in the supported set. AddCompilationRoot canonicalizes with
GetCanonMethodTarget(CanonicalFormKind.Specific), which only maps reference types
to __Canon - a SignatureMethodVariable passes through unchanged, so the typical
definition gets rooted and queued, and compilation then fails as soon as the body
resolves a token mentioning !!0:

  Unable to cast object of type 'Internal.TypeSystem.SignatureMethodVariable'
  to type 'Internal.TypeSystem.DefType'

No existing hardware intrinsic class has generic methods, so this had never been
exercised. Skip them; instantiations that are actually used are rooted through
their callers.

This lets Lse.cs go back to the conventional recursive intrinsic stubs.

Verified: composite crossgen of CoreLib with the CI command line (mibc,
--embed-pgo-data) now succeeds for arm64 on maccatalyst, osx, linux and windows,
with the recursive stubs restored. NativeAOT linux-arm64 light-up is unchanged
(tbnz dotnet#6 plus swpal/casal/ldaddal/ldsetal/ldclral, no out-of-line calls), an
unsupported type argument still compiles to a throw helper rather than recursing,
and SPMI asmdiffs remain at 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec0dae32-314b-42fd-a971-95921ea8ebb4
Copilot AI review requested due to automatic review settings August 15, 2026 14:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

@EgorBo

EgorBo commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

/azp list

@azure-pipelines

This comment was marked as resolved.

@EgorBo

EgorBo commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@MichalPetryka

Copy link
Copy Markdown
Contributor

Does it make sense to share the same intrinsic definitions for ARM64, RISCV and Loongarch instead?

@EgorBo

EgorBo commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

Does it make sense to share the same intrinsic definitions for ARM64, RISCV and Loongarch instead?

Those are not exactly the same isa, are they? LSE has (and LSE2) have quite a few things we might expose in the future, i don't think we can share anything (although, some of the changes in JIT I made are shared).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[NativeAOT Arm64] Opportunistic light-up for Atomics instructions

5 participants