Skip to content

ARM64: elide redundant self moves after selected 32-bit ALU operations - #134646

Open
Garume wants to merge 1 commit into
dotnet:mainfrom
Garume:jit/arm64-redundant-alu-mov
Open

Garume wants to merge 1 commit into
dotnet:mainfrom
Garume:jit/arm64-redundant-alu-mov

Conversation

@Garume

@Garume Garume commented Sep 25, 2026

Copy link
Copy Markdown

Related to #134538.

A 32-bit ALU instruction already clears the upper half of its destination register, so a following mov Wd, Wd can be redundant. This extends the existing load check in emitter::IsRedundantMov to the IF_DR_3A forms of add, mul, lsl, eor, and and.

For example, the sequence for (ulong)(left + right), with uint operands, changes from:

add w0, w0, w1
mov w0, w0

to:

add w0, w0, w1

The check requires a 32-bit producer writing the same general-purpose register, and uses the existing instruction-boundary guard. It excludes SP and checks the instruction format as well as the opcode. This is a partial implementation of the issue: other ALU opcodes and immediate forms are left for a separate change.

The tests cover unsigned results with bit 31 set, overflow, shift counts, and AND operands. They also check that signed widening still sign-extends and that 64-bit results retain their upper bits unless explicitly narrowed.

Validation

Tested on macOS ARM64 against a5f96c5d7120969f9e22871a763c3d30567c7d84:

  • Built the baseline and changed JITs in Checked and Release.
  • Ran all six regression test methods through the runtime's generated standalone test runner with each of those four JITs. All passed. These runs used the matching Release CoreRun and libraries, with tiering and ReadyToRun disabled.
  • Checked the generated code for the affected operations.

SuperPMI asmdiffs with the Checked JITs and loop alignment enabled:

Collection Replayed contexts Smaller Larger Same size, different assembly Bytes saved
realworld.run.osx.arm64.checked 28,871 144 0 6 1,528
benchmarks.run.osx.arm64.checked 67,761 282 0 2 1,704
Total 96,632 426 0 8 3,232

Both JITs replayed every context successfully, with no misses or errors. The collections can contain overlapping methods. The full runtime test suite has not been run locally.

These are code-size results. No whole-workload throughput improvement is claimed.

Developed with Codex assistance.

@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 Sep 25, 2026
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Sep 25, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 6 pipeline(s).
10 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.

@AndyAyersMS

Copy link
Copy Markdown
Member

@dhartglassMSFT fyi

@Garume how is this different than the prototype patch on #134538 (expand at bottom of initial comment). Seems like it handles fewer cases?

@Garume

Garume commented Sep 27, 2026

Copy link
Copy Markdown
Author

Yes, this covers fewer cases than your prototype. I restricted it to the IF_DR_3A forms of add, mul, lsl, eor, and and after seeing slower UDIV and MADD microbenchmarks on my M4 Pro. I should have included those measurements in the description.

I've now compared the exact prototype and this PR on the same base (a5f96c5d7120) and the same 96,632 SuperPMI contexts:

Patch Smaller contexts Larger contexts Bytes saved
This PR 426 0 3,232
Issue prototype 1,235 0 9,640

Both replayed without misses or errors. The expanded regression tests also passed with Checked and Release JITs, including separate Checked runs with JitStress=2. I haven't found a correctness issue with the prototype.

Here are the new BDN results. Each value is the average of three launch means, in ns/op; lower is faster.

Benchmark Baseline This PR Issue prototype Prototype vs. baseline
Add 0.70137 0.64611 0.64215 -8.44%
Divide (UDIV) 0.66101 0.66331 0.68579 +3.75%
Load32 (control) 0.67933 0.69091 0.68197 +0.39%
AndRegister 0.69699 0.67080 0.66707 -4.29%
AndImmediate 0.59141 0.61676 0.58489 -1.10%
MultiplyAdd (MADD) 0.51114 0.51069 0.67838 +32.72%

Apple M4 Pro, macOS 26.5.1, BDN 0.16.0-nightly.20260801.595, Release JITs with matching CoreRun/libraries. Tiering, TieredPGO, and ReadyToRun were disabled. I ran A/A calibration, then ABC/BCA/CAB (A=baseline, B=this PR, C=prototype), with 5 warmups, 10 measured iterations, and a 200 ms target iteration time per launch. The calibration runs are excluded from the table.

MADD was slower in all three prototype launches: 0.6709-0.6897 ns, versus 0.5055-0.5172 ns for the baseline. The timed method differs only by the removed self-move; the BDN loops are unchanged. I haven't identified the CPU-level cause.

The UDIV difference is less conclusive: the A/A run also varied by 3.26%. AndImmediate produced identical code in the baseline and this PR, yet their measured means differed by about 4.29%. Background services were still running, and BDN could not elevate process priority. Small differences in this table should therefore be treated cautiously. These are tiny microbenchmarks, not application throughput results.

Those measurements explain why I held back UDIV and MADD, but they don't justify leaving out all the other forms.

Individual launch means, including calibration

A = baseline; B = this PR; C = issue prototype. Values are launch means in ns/op. The first two passes are A/A calibration and are excluded from the aggregate table.

Pass Variant Calibration Add Divide Load32 AndRegister AndImmediate MultiplyAdd
0 A Yes 0.71241 0.67798 0.67302 0.69114 0.58163 0.50639
1 A Yes 0.69744 0.65587 0.67433 0.69837 0.59109 0.50646
2 A No 0.70108 0.65730 0.67805 0.69841 0.58851 0.51724
3 B No 0.63776 0.65642 0.68142 0.64115 0.58884 0.51019
4 C No 0.63913 0.68199 0.68024 0.67694 0.57769 0.67453
5 B No 0.63910 0.65764 0.69243 0.68346 0.65971 0.51172
6 C No 0.64212 0.68582 0.68276 0.68030 0.59551 0.68972
7 A No 0.70707 0.66474 0.68226 0.69672 0.59316 0.51069
8 C No 0.64520 0.68956 0.68291 0.64397 0.58148 0.67088
9 A No 0.69595 0.66101 0.67767 0.69583 0.59257 0.50549
10 B No 0.66148 0.67586 0.69890 0.68780 0.60174 0.51016
MADD benchmark and actual timed assembly

The measured method and its input fields, excerpted from ExpandedBench:

internal uint left = 0xffff_ffffU;
internal uint right = 3U;
internal uint third = 5U;

[Benchmark]
public ulong MultiplyAdd()
{
    uint result = unchecked(left * right + third);
    return result;
}

Baseline and this PR (32 bytes):

stp fp, lr, [sp, #-0x10]!
mov fp, sp
ldp w1, w2, [x0, #0x10]
ldr w0, [x0, #0x18]
madd w0, w1, w2, w0
mov w0, w0
ldp fp, lr, [sp], #0x10
ret lr

Issue prototype (28 bytes):

stp fp, lr, [sp, #-0x10]!
mov fp, sp
ldp w1, w2, [x0, #0x10]
ldr w0, [x0, #0x18]
madd w0, w1, w2, w0
ldp fp, lr, [sp], #0x10
ret lr

The surrounding BDN workload loops were identical across all three variants. This excerpt omits the unchanged fixture setup; the complete source is preserved separately.

Recorded benchmark command

This is the baseline command from the first comparison pass, after A/A calibration. The other passes substitute the corresponding CoreRun host and a separate output directory. All three hosts use the same CoreRun and libraries; only the JIT differs. Paths below are local to the measurement machine.

/Users/Shared/perf-lab/perfbuild/runtime-jit-134538/base/.dotnet/dotnet /Users/Shared/perf-lab/perfbuild/runtime-jit-134538/throughput-1/fixture/bin/Release/net11.0/Validate.dll --filter DirectBench.Add DirectBench.Divide DirectBench.Load32 ExpandedBench.AndRegister ExpandedBench.AndImmediate ExpandedBench.MultiplyAdd --coreRun /Users/Shared/perf-lab/perfbuild/runtime-jit-134538/pr-validation/hosts/base-release/corerun --cli /Users/Shared/perf-lab/perfbuild/runtime-jit-134538/base/.dotnet/dotnet --artifacts /Users/Shared/perf-lab/perfbuild/runtime-jit-134538/prototype-review/timing-2-A/bdn --warmupCount 5 --iterationCount 10 --iterationTime 200 --launchCount 1

DOTNET_TieredCompilation=0, DOTNET_TieredPGO=0, and DOTNET_ReadyToRun=0 were set. The executable was built from the unchanged fixture using its pinned SDK and BDN package.

This branch has not been deployed

No deployments
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 community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants