[mono][amd64] Pass small straddling vtypes in registers in the LLVM backend#129702
Open
pavelsavara wants to merge 5 commits into
Open
[mono][amd64] Pass small straddling vtypes in registers in the LLVM backend#129702pavelsavara wants to merge 5 commits into
pavelsavara wants to merge 5 commits into
Conversation
…ackend On amd64 a <=16 byte all-integer value type whose nested field crosses the 8-byte SysV eightbyte boundary (e.g. Nullable<T> where T is an 8-byte struct, giving value@4 size 8) is forced onto the stack (ArgOnStack -> LLVMArgVtypeByVal) because the JIT cannot place a straddling field in a register pair. The concrete, fully-flattened instantiation of the same layout has no straddling field and is passed in two integer registers (ArgValuetypeInReg). These two classifications must agree because partially-shared generic code calls the concrete method: the partially-shared caller sees the value type as an opaque type parameter (one straddling field -> byval) while the concrete callee sees the flattened layout (-> registers). Older LLVM passed the small byval struct in registers so they happened to match; newer LLVM lowers byval onto the stack, so the caller writes the value to the stack while the callee reads it from registers, corrupting it (e.g. box/unbox of Nullable<struct-with-nullable-field> in gshared code returns the wrong value). Mark such all-integer <=16 byte straddling vtypes so the LLVM backend passes them in two integer registers, matching the flattened classification. The JIT path is left unchanged. Only amd64 is affected: arm64 classifies by size (no straddle rule) and uses a register-array representation, and wasm/llvmonly use a different, already-correct nullable box path.
Contributor
|
Tagging subscribers to this area: @steveisok, @vitek-karas |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adjusts Mono’s amd64 (SysV) calling-convention classification for the LLVM backend so that certain small (≤16-byte) all-integer value types that were previously forced onto the stack due solely to “straddling field” layout are instead passed in integer registers, aligning the opaque/gshared classification with the fully-flattened classification and avoiding ABI mismatches in LLVM AOT codegen.
Changes:
- Add an
ArgInfo.llvm_inreg_straddleflag to mark “stack-only due to straddle” small all-integer structs. - In
add_valuetype(), detect straddling fields and setllvm_inreg_straddlefor eligible non-return structs. - In
mono_arch_get_llvm_call_info(), when an argument isArgOnStackbut markedllvm_inreg_straddle, pass it asLLVMArgVtypeInReg(two integer registers) instead ofLLVMArgVtypeByVal.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/mono/mono/mini/mini-amd64.h | Adds a new ArgInfo bitflag to annotate the “straddle-induced stack” case for LLVM. |
| src/mono/mono/mini/mini-amd64.c | Computes the new flag during SysV vtype classification and uses it to select an LLVM in-register lowering for affected arguments. |
7 tasks
With the amd64 LLVM backend now passing small straddling vtypes in registers, these tests pass under Mono full-AOT. Remove the ActiveIssue annotation so CI exercises the fix.
pavelsavara
added a commit
that referenced
this pull request
Jun 22, 2026
Re-link the disabled Mono full-AOT tests from the #129508 tracking issue to the individual PRs that fix them (nullabletypes -> #129702, call05_large/small -> #129708, WPF_3226 -> #129710, b143840 -> #129713, UnitTest_GVM_TypeLoadException -> #129715). The tests stay disabled; Runtime_105619 keeps the #129508 link.
… changes mini-amd64.* (and other arch backends), mini-exceptions.*, exceptions-*.* and mini-runtime.* feed Mono LLVM full-AOT codegen but were not in the runtime-llvm PR path filter, so PRs touching only those files (e.g. this one) skipped the LLVMFULLAOT validation leg.
The straddle-in-register optimization exists to make a managed call's convention match a fully-flattened instantiation (gshared/partially-shared generics). P/Invoke must follow the native ABI via the byval/stack path, so don't set llvm_inreg_straddle for pinvoke signatures; they keep the prior ArgOnStack -> LLVMArgVtypeByVal lowering. Addresses review feedback.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On amd64, a
Γëñ16-byte all-integer value type whose nested field crosses the 8-byte SysV eightbyte boundary is passed on the stack as abyvalstruct by the Mono LLVM backend, while a fully-flattened instantiation of the same layout is passed in two integer registers. These two classifications must agree because partially-shared generic code calls the concrete method, and recent LLVM lowers smallbyvalstructs onto the stack instead of into registers ΓÇö so the value is corrupted across the call. In practice this shows up only under Mono LLVM full-AOT on x64, e.g. boxing/unboxing aNullable<struct-with-a-nullable-field>in shared generic code returns the wrong value.Minimal repro
100.666ΓÇö the boxed/unboxed value is corrupted.Root cause
add_valuetype(mini-amd64.c) forces a≤16-byte value type onto the stack (ArgOnStack→LLVMArgVtypeByVal) when a nested field straddles the 8-byte eightbyte boundary, because the JIT cannot place a straddling field in a register pair. ForNullable<GenQ<int>>the value field is at offset 4, size 8 (it crosses byte 8).The concrete, fully-flattened instantiation (
{ bool@0, bool@4, int@8 }) has no straddling field and is passed in two integer registers (ArgValuetypeInReg).Partially-shared generic code calls the concrete method, so the conventions must match: the partially-shared caller sees the value type as an opaque type parameter (one straddling field →
byval) while the concreteNullable<T>:Boxcallee sees the flattened layout (→ registers). Older LLVM passed the smallbyvalstruct in registers so they matched; LLVM 18+ lowersbyvalonto the stack, so the caller writes the value to the stack while the callee reads it from registers → corruption.Fix
Mark all-integer
Γëñ16-byte straddling vtypes (ArgInfo.llvm_inreg_straddle) and haveget_llvm_call_infopass them in two integer registers (LLVMArgVtypeInReg), matching the flattened classification and restoring the pre-LLVM-18 behavior. The non-LLVM JIT path is unchanged; float-containing structs keep thebyvalpath.Scope x64 only
LLVMArgAsIArgs), so caller and callee already agree.Validation
Built Mono + LLVM full-AOT on x64 and ran the
JIT/Directed/nullabletypestest sources (castclassvaluetype, which covers every nullable type —int?…Guid?, the generic-struct cases, andfloat?/double?/decimal?): all pass under both full-AOT and JIT with the fix; without it the generic-struct nullable cases fail. The minimal repro above goes from666→100.Alternatives considered
Γëñ16-byte INTEGERbyvalin registers (restore the old lowering). Uniform, but lives indotnet/llvm-projectand re-introduces reliance on non-standardbyvalregister passing.cfg->gsharedvt == 0), whose call passes the valuebyval(not by-ref), so the out-wrapper expects the wrong convention and faults.byvallowering.Note
This pull request was prepared with the assistance of GitHub Copilot.
Part of the MonoAOT LLVM 23 full-AOT regression set tracked by #129508. Built and tested together with the Emscripten 5.0.6 / LLVM 23 bump on #129396, where the re-enabled
nullabletypestests (boxunboxvaluetype / castclassvaluetype) pass on theruntime-llvmAllSubsets_Mono_LLVMFULLAOT_RuntimeTestsleg.Contributes to #129508.