Skip to content

JIT: Enable inlining for late devirtualization - #110827

Merged
AndyAyersMS merged 61 commits into
dotnet:mainfrom
hez2010:inline-exact-devirt
Jan 29, 2025
Merged

JIT: Enable inlining for late devirtualization#110827
AndyAyersMS merged 61 commits into
dotnet:mainfrom
hez2010:inline-exact-devirt

Conversation

@hez2010

@hez2010hez2010 commented Dec 18, 2024

Copy link
Copy Markdown
Contributor

If we see a new inline candidate after late devirtualization, we can try marking and inlining it.
This unblocks the inlining and stack-allocating arbitrary ref-class enumerators (unless the inliner considers GetEnumerator as non-profitable). We might need to tune the inliner heuristics later to get more profitable inlining opportunities.

Contributes to #7541 and #108913

There're some really nice diffs: https://gist.github.com/MihuBot/29f7c64533ac1f38494fbfab361ab505

Example:

[MethodImpl(MethodImplOptions.NoInlining)]staticfloatGetDistance(){IVectorp1=Vector.GetVector(4,2);IVectorp2=Vector.GetVector(1,6);IVectordir=p2.Sub(p1);returnMathF.Sqrt(dir.X*dir.X+dir.Y*dir.Y);}structVector(floatx,floaty):IVector{publicfloatX{get;set;}=x;publicfloatY{get;set;}=y;publicreadonlyIVectorSub(IVectorother)=>GetVector(X-other.X,Y-other.Y);publicstaticIVectorGetVector(floatx,floaty)=>newVector(x,y);}interfaceIVector{IVectorSub(IVectorother);floatX{get;set;}floatY{get;set;}}

Before:

; Assembly listing for method Program:GetDistance():int (FullOpts)G_M12138_IG01: ;; offset=0x0000pushrbxsubrsp,48 ;; size=5 bbWeight=1 PerfScore 1.25G_M12138_IG02: ;; offset=0x0005movrcx,0x7FFD37F391F8 ; Vectorcall CORINFO_HELP_NEWSFASTmovrdx,raxmovrcx,0x4000000040800000mov qword ptr [rdx+0x08],rcxmov dword ptr [rsp+0x20],0x3F800000mov dword ptr [rsp+0x24],0x40C00000learcx,[rsp+0x20]call[Vector:Sub(IVector):IVector:this]movrbx,raxmovrcx,rbxmovr11,0x7FFD36EE0370 ; code for IVector:get_X():float:thiscall[r11]IVector:get_X():float:thisvmovss dword ptr [rsp+0x2C],xmm0movrcx,rbxmovr11,0x7FFD36EE0378 ; code for IVector:get_X():float:thiscall[r11]IVector:get_X():float:this vmulss xmm0,xmm0, dword ptr [rsp+0x2C]vmovss dword ptr [rsp+0x2C],xmm0movrcx,rbxmovr11,0x7FFD36EE0380 ; code for IVector:get_Y():float:thiscall[r11]IVector:get_Y():float:thisvmovss dword ptr [rsp+0x28],xmm0movrcx,rbxmovr11,0x7FFD36EE0388 ; code for IVector:get_Y():float:thiscall[r11]IVector:get_Y():float:this vmulss xmm0,xmm0, dword ptr [rsp+0x28] vaddss xmm0,xmm0, dword ptr [rsp+0x2C] vsqrtss xmm0,xmm0,xmm0 ;; size=166 bbWeight=1 PerfScore 51.50G_M12138_IG03: ;; offset=0x00ABaddrsp,48poprbxret ;; size=6 bbWeight=1 PerfScore 1.75

GDV:

; Assembly listing for method Program:GetDistance():int (Tier1)G_M12138_IG01: ;; offset=0x0000pushrsipushrbxsubrsp,40 ;; size=6 bbWeight=1 PerfScore 2.25G_M12138_IG02: ;; offset=0x0006movrbx,0x7FFD4531D278 ; Vectormovrcx,rbxcall CORINFO_HELP_NEWSFASTmovrsi,raxmovrcx,0x4000000040800000mov qword ptr [rsi+0x08],rcxmovrcx,rbxcall CORINFO_HELP_NEWSFASTmovrcx,0x40C000003F800000mov qword ptr [rax+0x08],rcxaddrax,8vmovssxmm0, dword ptr [rax] vsubss xmm0,xmm0, dword ptr [rsi+0x08]vmovssxmm1, dword ptr [rax+0x04] vsubss xmm1,xmm1, dword ptr [rsi+0x0C]vmovss dword ptr [rsp+0x24],xmm0vmovss dword ptr [rsp+0x20],xmm1movrcx,rbxcall CORINFO_HELP_NEWSFASTvmovssxmm0, dword ptr [rsp+0x24]vmovss dword ptr [rax+0x08],xmm0vmovssxmm1, dword ptr [rsp+0x20]vmovss dword ptr [rax+0x0C],xmm1vmovssxmm0, dword ptr [rax+0x08] vmovaps xmm1,xmm0 vmulss xmm0,xmm1,xmm0vmovssxmm1, dword ptr [rax+0x0C] vmovaps xmm2,xmm1 vmulss xmm1,xmm2,xmm1 vaddss xmm0,xmm1,xmm0 vsqrtss xmm0,xmm0,xmm0 ;; size=156 bbWeight=1 PerfScore 67.50G_M12138_IG03: ;; offset=0x00A2addrsp,40poprbxpoprsiret ;; size=7 bbWeight=1 PerfScore 2.25

After:

; Assembly listing for method Program:GetDistance():int (FullOpts)G_M12138_IG01: ;; offset=0x0000 ;; size=0 bbWeight=1 PerfScore 0.00G_M12138_IG02: ;; offset=0x0000vmovssxmm0, dword ptr [reloc @RWD00] ;; size=8 bbWeight=1 PerfScore 3.00G_M12138_IG03: ;; offset=0x0008ret ;; size=1 bbWeight=1 PerfScore 1.00RWD00 dd 40A00000h ; 5

/cc: @AndyAyersMS

@hez2010

Copy link
Copy Markdown
ContributorAuthor

@MihuBot

@hez2010

Copy link
Copy Markdown
ContributorAuthor

@MihuBot

@hez2010

Copy link
Copy Markdown
ContributorAuthor

cc @AndyAyersMS
By enabling this we managed to inline all kinds of ref-class enumerators and stack allocated them: https://gist.github.com/MihuBot/0a4cb4714afc1ff5fe474f99df1ce6dd

@hez2010
hez2010 marked this pull request as ready for review January 21, 2025 10:48
@AndyAyersMS

Copy link
Copy Markdown
Member

Skimmed the changes and it seems promising. I'll take a deeper look soon.

@AndyAyersMSAndyAyersMS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM -- @jakobbotsch any other feedback?

@hez2010

Copy link
Copy Markdown
ContributorAuthor

I'm testing TP impact of splitting trees in #111896. If it turns out that the TP impact has the similar pattern with the check in this PR, I will merge it into this branch.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM as well as is

@hez2010

hez2010 commented Jan 28, 2025

Copy link
Copy Markdown
ContributorAuthor

Seems that TP with splitting the tree doesn't have meaningful change: https://dev.azure.com/dnceng-public/public/_build/results?buildId=931974&view=ms.vss-build-web.run-extensions-tab

Merged the commits into this PR.

Comment threadsrc/coreclr/jit/fginline.cpp Outdated
{
Statement* newStmt = nullptr;
GenTree** callUse = nullptr;
if (m_compiler->gtSplitTree(m_compiler->compCurBB, m_curStmt, call, &newStmt, &callUse))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gtSplitTree needs to learn that it has to split lvHasLdAddrOp locals when it runs early.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment threadsrc/coreclr/jit/fginline.cpp Outdated
{
Statement* newStmt = nullptr;
GenTree** callUse = nullptr;
if (m_compiler->gtSplitTree(m_compiler->compCurBB, m_curStmt, call, &newStmt, &callUse, true))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you using context != nullptr as a check if devirtualization succeeded? A better check is !call->IsVirtual().

We should avoid splitting the tree if devirtualization fails, and also if the call can't be made into an inline candidate (seems like impMarkInlineCandidate can be called on a non-root call...?)

So maybe reorder things a bit.

I'd also be fine if you revert all this and go back to the non-split version.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it's not obvious that splitting a tree that we're walking will work out as expected. I think it's ok here, especially since we back up and will reprocess the current tree, but it feels like it would be cleaner if we aborted the walk at this point.

@hez2010hez2010Jan 28, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah context will be nullptr if devirtualization failed.
Switched to use !call.IsVirtual() instead. The context will be used while calling impMarkInlineCandidate so I added an assertion to make sure it's correctly set.

Resolved.

@AndyAyersMSAndyAyersMS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. Will give @jakobbotsch one last chance to look too.

{
Statement* newStmt = nullptr;
GenTree** callUse = nullptr;
if (m_compiler->gtSplitTree(m_compiler->compCurBB, m_curStmt, call, &newStmt, &callUse, true))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking this split is unnecessarily conservative: it will introduce temps for the arguments of the call, which can bypass optimizations the inliner can make depending on the specific shape of the argument. Fixing that requires enhancing gtSplitTree a bit (see e.g. my version in this branch), however, even with that done you will run into issues around incorrect treatment of the retbuffer in the inliner. So I wouldn't suggest trying to do anything about it at this point, but it is a potential future improvement.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@AndyAyersMS
AndyAyersMS merged commit a9d67ec into dotnet:mainJan 29, 2025
@AndyAyersMS

Copy link
Copy Markdown
Member

@hez2010 thanks again!

grendello added a commit to grendello/runtime that referenced this pull request Jan 30, 2025
* main:
Make cdac APIs public but experimental (dotnet#111180)
JIT: Enable inlining for late devirtualization (dotnet#110827)
Remove unsafe `bool` casts (dotnet#111024)
Fix shimmed implementation of TryGetHashAndReset to handle HMAC.
}

call->GetSingleInlineCandidateInfo()->exactContextHandle = context;
INDEBUG(call->GetSingleInlineCandidateInfo()->inlinersContext = call->gtInlineContext);

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.

@hez2010 I think this Debug logic is causing Debug and Release JITs to report differing amounts of metadata to SuperPMI. runtime-coreclr superpmi-asmdiffs-checked-release broke around the time this PR was merged, and I'm not getting any diffs locally when using builds from the commit before this one.

cc @AndyAyersMS@jakobbotsch. I'm not sure what the ideal fix is here, since GenTreeCall::gtInlineContext is Debug-only, and removing this logic altogether triggers Debug asserts in InlineStrategy::NewContext.

@hez2010hez2010Feb 10, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may need to move call->gtInlineContext out of #if DEBUG and always set call->GetSingleInlineCandidateInfo()->inlinersContext accordingly, otherwise inlinersContext can sometimes be incorrect because we don't keep tracking it after the importer is done, and leading to inlining failure (which will be caught silently and reverted so it won't affect the correctness of codegen). This may explain why you are seeing different amounts of metadata to SuperPMI, where in Release we inline fewer methods due to the incorrect inlinersContext.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the information be put into LateDevirtualizationInfo?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have avoided trying to add more fields to GenTreeCall that are only needed for some small subset of all calls.

What @jakobbotsch suggests makes sense to me.

@hez2010hez2010Feb 11, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I revised this issue and found that the issue is that the debug logic should really be

INDEBUG(call->gtInlineContext = call->GetSingleInlineCandidateInfo()->inlinersContext);

instead.

We set a new inlinersContext for the inline candidate, but didn't update the original InlineContext for the call. Sorry for this dumb typo mistake... I should really have noticed this before.

The fix is now live: #112396

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 SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@hez2010@AndyAyersMS@jakobbotsch@amanasifkhalid