dotnet / dotnet/runtime

[ARM64] R2R methods require RCPC2 without using RCPC2 instructions

Open
#133,671 6 comments 0 reactions 0 assignees View on GitHub
area-CodeGen-coreclr untriaged
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

## Description

Crossgen2 adds RCPC2 dependencies to methods that contain no RCPC2 instructions, causing unnecessary R2R rejection on CPUs without RCPC2.

Context: https://github.com/dotnet/android/pull/12722?overview=false#issuecomment-5615675089

## Reproduction

Create `Probe/Probe.csproj`:

```xml


net11.0
true

```

Add `Probe/Probe.cs`:

```csharp
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
public sealed class Fields
{
[FieldOffset(0)]
public volatile int Small;

[FieldOffset(320)]
public volatile int Large;
}

public sealed class ReferenceField
{
public volatile object Value;
}

public static class Probe
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static int ReadSmall(Fields value) => value.Small;

[MethodImpl(MethodImplOptions.NoInlining)]
public static int ReadLarge(Fields value) => value.Large;

[MethodImpl(MethodImplOptions.NoInlining)]
public static void WriteSmall(Fields value, int data) => value.Small = data;

[MethodImpl(MethodImplOptions.NoInlining)]
public static void WriteLarge(Fields value, int data) => value.Large = data;

[MethodImpl(MethodImplOptions.NoInlining)]
public static void WriteReference(ReferenceField value, object data) => value.Value = data;
}
```

Publish:

```bash
dotnet publish Probe/Probe.csproj -c Release -r linux-arm64
```

Disassembly and dependency graphs for stock and RCPC1-only builds

Run from the directory containing `Probe/`. Clean between variants to avoid reusing R2R output:

```bash
set -euo pipefail
for policy in stock rcpc-only; do
output="$PWD/evidence/$policy"
mkdir -p "$output"
extra="--mapcsv;--parallelism:1;--dgmllog:$output/dependencies.dgml;--fulllog"
extra+=";--codegenopt:JitDisasm=*;--codegenopt:JitDisasmTesting=1"
extra+=";--codegenopt:JitDisasmWithCodeBytes=1;--codegenopt:JitStdOutFile=$output/jit-disasm.txt"
if [ "$policy" = rcpc-only ]; then
extra+=";--instruction-set:rcpc,-optimistic"
fi

escaped=${extra//;/%3B}
escaped=${escaped//,/%2C}
dotnet clean Probe/Probe.csproj -c Release -r linux-arm64
dotnet publish Probe/Probe.csproj -c Release -r linux-arm64 \
"-p:PublishReadyToRunCrossgen2ExtraArgs=$escaped" \
-o "$output/publish"
cp Probe/obj/Release/net11.0/linux-arm64/R2R/Probe.map.csv "$output/"
done
```

## Actual behavior

The stock build declares RCPC2 for all five methods, but only two use it:

| Method | Generated code | Uses RCPC2? |
| --- | --- | --- |
| `ReadSmall` | `ldapur w0, [x0, #8]` | Yes |
| `WriteSmall` | `stlur w1, [x0, #8]` | Yes |
| `ReadLarge` | `add x0, x0, #328; ldapr w0, [x0]` | **No** |
| `WriteLarge` | `add x0, x0, #328; stlr w1, [x0]` | **No** |
| `WriteReference` | Call to `CORINFO_HELP_ASSIGN_REF` | **No** |

`ReadLarge`:

```asm
stp x29, x30, [sp, #-0x10]!
mov x29, sp
add x0, x0, #328
ldapr w0, [x0]
ldp x29, x30, [sp], #0x10
ret
```

Despite using only RCPC1, it has this dependency:

```text
MethodWithGCInfo(Probe_Probe__ReadLarge)
-> PrecodeHelperImport->SignaturePointer_ReadyToRunInstructionSets_Rcpc+Rcpc2,
reason: classMustBeLoadedBeforeCodeIsRun
```

The emitted ISA signature also requires RCPC2. With `--instruction-set:rcpc,-optimistic`, `ReadLarge`, `WriteLarge` and `WriteReference` have **byte-identical machine code without that dependency**.

## Expected behavior

`ReadLarge`, `WriteLarge` and `WriteReference` should not require RCPC2.

## Likely cause

[`Lowering::TryCreateAddrMode`](https://github.com/dotnet/dotnet/blob/3c85e271beed873eb8be89baaf04f1c13f06dabe/src/runtime/src/coreclr/jit/lower.cpp#L7733-L7825) calls `compOpportunisticallyDependsOn(InstructionSet_Rcpc2)` before checking whether the addressing mode is usable.

That call [records the dependency](https://github.com/dotnet/dotnet/blob/3c85e271beed873eb8be89baaf04f1c13f06dabe/src/runtime/src/coreclr/jit/compiler.h#L10896-L10929), which survives when a later offset/index/containment check fails.

Here, offset 328 is outside the signed 9-bit range. The reference-store case also records RCPC2 even though codegen takes the [GC write-barrier path](https://github.com/dotnet/dotnet/blob/3c85e271beed873eb8be89baaf04f1c13f06dabe/src/runtime/src/coreclr/jit/codegenarm64.cpp#L4254-L4323).

## Impact and environment

In a MAUI sample-content composite, **117 of 564 RCPC2-guarded bodies contain no RCPC2 instruction**. The other 447 genuinely use RCPC2. Startup impact of a fix has not been measured.

Reproduced on a macOS ARM64 host, publishing a `net11.0` library for `linux-arm64`, with:

- .NET SDK `11.0.100-rc.1.26431.118`
- crossgen2/runtime `11.0.0-rc.1.26431.118`

The same check is present in [runtime main at `c307f70`](https://github.com/dotnet/runtime/blob/c307f70df227cc74d6c1dd50b663893431e74a6a/src/coreclr/jit/lower.cpp#L7726-L7785) (source inspection only).

[Investigation and discussion](https://github.com/dotnet/android/pull/12722#issuecomment-5615675089).

_Drafted with the Copilot App._

Contributor guide

Open the contributing guide

Research direction

Start with Lowering::TryCreateAddrMode in src/coreclr/jit/lower.cpp, then read the dependency recording in compiler.h and the ARM64 write-barrier path in codegenarm64.cpp. Run the supplied Probe publish and dependency/disassembly commands before changing behavior. Done means ReadLarge, WriteLarge, and WriteReference no longer require RCPC2 while methods that emit RCPC2 instructions still do.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, csharp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.