Skip to content

Inline nullable allocators - #122167

Merged
EgorBo merged 20 commits into
dotnet:mainfrom
EgorBo:inline-nullable-allocators
Jan 7, 2026
Merged

Inline nullable allocators#122167
EgorBo merged 20 commits into
dotnet:mainfrom
EgorBo:inline-nullable-allocators

Conversation

@EgorBo

@EgorBoEgorBo commented Dec 3, 2025

Copy link
Copy Markdown
Member

Closes#50915

Today we never inline & stack-allocate (via the escape analysis) boxed Nullable<>, let's see if we can fix that.

objectTest(long?n){returnn;}

Main:

; Method My:Test(System.Nullable`1[long]):System.Object:this (FullOpts)subrsp,40movrcx,0x7FFBCE1680C0 ; System.Nullable`1[long]call[CORINFO_HELP_BOX_NULLABLE]nopaddrsp,40ret; Total bytes of code: 26

PR:

; Method My:Test(System.Nullable`1[long]):System.Object:this (FullOpts)pushrbxsubrsp,32movrbx,rdxcmp byte ptr [rbx],0je SHORT G_M7100_IG04movrcx,0x7FFBDFBC08B0 ; System.Int64call CORINFO_HELP_NEWSFASTmovrcx, qword ptr [rbx+0x08]mov qword ptr [rax+0x08],rcxjmp SHORT G_M7100_IG05G_M7100_IG04:xorrax,raxG_M7100_IG05:addrsp,32poprbxret; Total bytes of code: 46

Another example (from #114497 (comment)) cc @pentp

publicstaticstring?Format<T>(Tvalue){if(valueisIFormattableformattable)returnformattable.ToString(null,null);returnnull;}

diff.
Doesn't allocate anymore to box that nullable.

Benchmarks: EgorBot/runtime-utils#563

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Dec 3, 2025
@EgorBo

This comment was marked as outdated.

Comment threadsrc/coreclr/jit/compiler.h Outdated
@EgorBo

Copy link
Copy Markdown
MemberAuthor

Enabled structs too (basically, all types, even shared), so now #114497 (comment) is properly handed.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -amd -arm

usingBenchmarkDotNet.Attributes;[MemoryDiagnoser][DisassemblyDiagnoser]publicclassBench{privateNullable<bool>_null=null;privateNullable<bool>_nonnull=true;[Benchmark]publicobjectBoxNull()=>_null;[Benchmark]publicobjectBoxNonNull()=>_nonnull;// https://github.com/dotnet/runtime/issues/50915privateS?_ns=(S?)default(S);[Benchmark]publicintIssue50915()=>CallM(_ns);staticintCallM<T>(Tt){if(tisIMyInterface)return((IMyInterface)t).M();return0;}}interfaceIMyInterface{intM();}structS:IMyInterface{publicintM()=>42;}

CopilotAI 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 optimizes nullable boxing operations by introducing early inline expansion instead of using helper calls. The optimization enables escape analysis to stack-allocate boxed nullable values when they don't escape the method, eliminating heap allocations in hot paths. The implementation adds a new early QMARK expansion phase that runs after import but before other optimizations, allowing subsequent passes to optimize the expanded control flow.

Key changes:

  • Inline expansion of nullable box operations in hot, optimized code paths using conditional allocation
  • New early QMARK expansion phase to enable object stack allocation optimizations
  • COMMA node splitting during QMARK expansion to improve optimization opportunities

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/importer.cppAdds inline expansion of nullable boxing with conditional allocation based on hasValue field
src/coreclr/jit/morph.cppEnhances QMARK expansion to support early phase, adds COMMA splitting, changes return type to PhaseStatus
src/coreclr/jit/compiler.cppAdds early QMARK expansion phase after import and updates late expansion call signature
src/coreclr/jit/compiler.hAdds OMF_HAS_EARLY_QMARKS flag and updates fgExpandQmarkNodes signature
src/coreclr/jit/compphases.hDefines new PHASE_EARLY_QMARK_EXPANSION phase

Comment threadsrc/coreclr/jit/morph.cpp
Comment threadsrc/coreclr/jit/importer.cpp
@EgorBo
EgorBoforce-pushed the inline-nullable-allocators branch from c1431db to ab99de1CompareDecember 5, 2025 21:27
@EgorBo
EgorBoforce-pushed the inline-nullable-allocators branch from 23f505f to 69f6243CompareDecember 6, 2025 01:30
@EgorBo

EgorBo commented Dec 6, 2025

Copy link
Copy Markdown
MemberAuthor

The superpmi diffs are obviously useless due to missing contexts. The normal jit-diff (e.g. this) is also a mess due to the way PMI works - we take all generics and try to instantiate them with one of the 7 predefined types (see here) and int? in that list which meant tons of boxings on top of nullable where nullable is never really used. If I remove from that list and only leave real usage, the diff become much more smaller and reasonable (it's expected for it to be a size increase obviously).

@EgorBo
EgorBoforce-pushed the inline-nullable-allocators branch from 8e2a401 to b2987b3CompareDecember 6, 2025 13:55
@EgorBo
EgorBo enabled auto-merge (squash) January 7, 2026 16:34
@EgorBo
EgorBo merged commit 242f7b2 into dotnet:mainJan 7, 2026
122 checks passed
@EgorBo
EgorBo deleted the inline-nullable-allocators branch January 7, 2026 16:35
@pentp

pentp commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

It seems that the allocations are elided only for primitive types, for structs there's still an allocation: https://godbolt.org/z/aTxMocvhW

@EgorBo

Copy link
Copy Markdown
MemberAuthor

It seems that the allocations are elided only for primitive types, for structs there's still an allocation: https://godbolt.org/z/aTxMocvhW

This problem is not related to nullable, it's a general limitation of the escape analysis, can be reproduced via:

staticstringTest1(Guidg){objecto=g;return(o).ToString();}staticstringTest2(intg){objecto=g;return(o).ToString();}

works for int, doesn't work for Guid 🙁

@EgorBo

Copy link
Copy Markdown
MemberAuthor

it will work if e.g. Guid.ToString is just a wrapper that calls a static string GuidToString(Guid g) method so jit can see it's passed by value

@pentp

pentp commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

But why is passing by reference a problem? It's still just a struct, so it can't escape anywhere...

@pentp

pentp commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

The example you provided doesn't actually allocate, it has something to do with Nullable specifically, not escape analysis: https://godbolt.org/z/4MEjcfraq

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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.

Nullable<T> interface check / dispatch is comparatively very slow

6 participants

@EgorBo@pentp@jkotas@AndyAyersMS@MichalStrehovsky