Arm64: don't fold ldr page offset (PAGEOFFSET_12L) for NativeAOT#129966
Arm64: don't fold ldr page offset (PAGEOFFSET_12L) for NativeAOT#129966EgorBo wants to merge 3 commits into
Conversation
The adrp+add+ldr to adrp+ldr[#:lo12:] fold uses a 64-bit PAGEOFFSET_12L reloc (R_AARCH64_LDST64_ABS_LO12_NC), whose scaled offset requires the target to be 8-byte aligned. R2R loads always go through pointer-aligned indirection cells, but NativeAOT can fold a direct 64-bit load of byte-packed frozen data (4-byte aligned), which the linker rejects with 'improper alignment for relocation'. Restrict the fold to non-NativeAOT. Fixes dotnet#129936 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib |
There was a problem hiding this comment.
Pull request overview
This PR adjusts the Arm64 emitter’s TryFoldPageOffsetIntoLdr peephole optimization to avoid emitting PAGEOFFSET_12L relocations when targeting the NativeAOT ABI, preventing generation of linker-invalid R_AARCH64_LDST64_ABS_LO12_NC relocations in scenarios where the referenced symbol may not be 8-byte aligned.
Changes:
- Disable
adrp+add+ldr→adrp+ldr[#:lo12:]folding for NativeAOT by early-returning inTryFoldPageOffsetIntoLdr. - Add an in-code rationale describing the alignment requirement of
PAGEOFFSET_12L/R_AARCH64_LDST64_ABS_LO12_NCand why NativeAOT differs.
Co-authored-by: Jakob Botsch Nielsen <Jakob.botsch.nielsen@gmail.com>
| return false; | ||
| } | ||
|
|
||
| // PAGEOFFSET_12L requires alignment which is not guaranteed for NAOT |
There was a problem hiding this comment.
@jkotas I'd assumed all relocatable constants in R2R go through (pointer-aligned) indirection slots. I tried to trick it with misaligned reads/writes to RVA data, but RVA blobs are always pointer-aligned too (requiredAlignment = targetPointerSize in GetRvaData). The failing case is actually NAOT statics: NAOT addresses non-GC statics directly and aligns the static region only to the largest static field's alignment (NonGCStaticFieldAlignment), which is 4 here (struct S { uint A, B; }). JIT's Physical promotion then does a 64-bit load of the struct at that 4-aligned address, which we fold into ldr [x],#:lo12: and LDST64_ABS_LO12 can't encode a non-8-aligned offset. R2R only ever directly addresses RVA data (pointer-aligned); regular statics go through helpers, so it's safe there.
If you think this introduces a fragility I can revert it, I just assumed the diffs (e.g. -200kb from corelib's size) was worth it.
There was a problem hiding this comment.
A separate question is whether it makes sense to increase alignment for statics to 8 bytes (or even 16) as well for perf since JIT loves to optimize struct loads with wider loads🤔 (to avoid perf penalty from misalignment)

cc @MichalStrehovsky
On coreclr, presumably, all static fields holding structs are pointer-size aligned (boxed objects).
There was a problem hiding this comment.
If you think this introduces a fragility
Yes, I think this implementation is fragile. It assumes an invariant that is not guaranteed by the system. I think it is just a matter of time until we run into an invalid reloc with R2R too here. A better way to do this would be to e.g. add JIT/EE interface API that returns the alignment of the blob.
-200kb from corelib's size
I like the code size reduction
it makes sense to increase alignment for statics to 8 bytes
It may help a bit with NAOT statics, but it won't fix the problem for instance fields (for both JIT and AOT). Correct?
The adrp+add+ldr -> adrp+ldr[#:lo12:] fold uses R_AARCH64_LDST64_ABS_LO12_NC, which encodes the :lo12: page offset scaled by 8, so the reloc target must be 8-byte aligned. Replace the blanket NativeAOT opt-out with a new JIT-EE API, getAddressAlignment(void*), and only fold when the VM guarantees the target is at least 8-byte aligned. - VM (CoreCLR/NGen): reports the alignment of the absolute address. - crossgen2 (R2R): reports pointer size; direct relocatable loads only target pointer-aligned data. - NativeAOT: reports the exact alignment of non-GC statics (the failing case) and pointer size for GC-statics/RVA data, and fails closed otherwise so an unaligned relocation can never be emitted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MichalStrehovsky
left a comment
There was a problem hiding this comment.
Thank you for making it work for native AOT!
| case NonGCStaticsNode nonGcStatics: | ||
| // The non-GC static region is aligned only to the largest static field's | ||
| // alignment (plus the cctor-context's pointer alignment when present), which | ||
| // can be smaller than the pointer size for byte-packed layouts. | ||
| LayoutInt fieldAlignment = nonGcStatics.Type.NonGCStaticFieldAlignment; | ||
| int baseAlignment = fieldAlignment.IsIndeterminate ? pointerSize : fieldAlignment.AsInt; | ||
| alignment = nonGcStatics.HasCCtorContext ? Math.Max(baseAlignment, pointerSize) : baseAlignment; | ||
| break; | ||
|
|
||
| case GCStaticsNode: | ||
| case FieldRvaDataNode: | ||
| // These are laid out with at least pointer-size alignment. | ||
| alignment = pointerSize; | ||
| break; |
There was a problem hiding this comment.
Could you add a IObjectNodeWithAlignment interface with a int GetAlignment(NodeFactory factory) method and have these nodes implement it? Then make sure GetAlignment is actually the number that gets reported from GetObjectData/GetDehydratableData. This is duplicating the logic in GetXXXData and runs the risk of getting out of sync.
| private uint getAddressAlignment(void* address) | ||
| { | ||
| if (address == null) | ||
| { | ||
| return 1; | ||
| } | ||
| #if READYTORUN | ||
| // In R2R, relocatable direct loads only target pointer-aligned data (RVA blobs and | ||
| // indirection cells); statics and other data are reached through helpers. Report | ||
| // pointer-size alignment, matching how such targets are laid out. | ||
| return (uint)_compilation.TypeSystemContext.Target.PointerSize; | ||
| #else | ||
| if (HandleToObject(address) is ISymbolNode node) | ||
| { | ||
| return (uint)GetSymbolNodeAlignment(node); | ||
| } | ||
|
|
||
| // Unknown target: report unaligned so the JIT avoids alignment-sensitive relocations. | ||
| return 1; | ||
| #endif |
There was a problem hiding this comment.
If the body is this different, let's put these into CorInfoImpl.ReadyToRun.cs (R2R) and CorInfoImpl.RyuJIT.cs (native AOT) instead and skip the ifdefs.
Fixes #129936.
PR #129589 added an Arm64 fold of
adrp+add+ldrintoadrp+ldr[#:lo12:]via a 64-bitPAGEOFFSET_12Lreloc (R_AARCH64_LDST64_ABS_LO12_NC). That reloc encodes the offset scaled by 8, so the target must be 8-byte aligned. In R2R the relocatable loads always go through pointer-aligned indirection cells, but NativeAOT can fold a direct 64-bit load of byte-packed frozen data (4-byte aligned), whichld.lldrejects:The fold is purely a size optimization, so this restricts it to non-NativeAOT and falls back to the always-safe
adrp+add+ldr.Validation (SPMI, linux-arm64):
benchmarks.run(R2R, ~427K contexts): 0 asm diffs — the win is preserved.smoke_tests.nativeaot(18891 ctx): clean replay, no diffs.