') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); JIT: Optimize out write barriers for fields in ref-like structs by EgorBo · Pull Request #103503 · dotnet/runtime · GitHub
Skip to content

JIT: Optimize out write barriers for fields in ref-like structs - #103503

Merged
EgorBo merged 2 commits into
dotnet:mainfrom
EgorBo:optimize-byref-structs
Jun 27, 2024
Merged

JIT: Optimize out write barriers for fields in ref-like structs#103503
EgorBo merged 2 commits into
dotnet:mainfrom
EgorBo:optimize-byref-structs

Conversation

@EgorBo

@EgorBoEgorBo commented Jun 15, 2024

Copy link
Copy Markdown
Member

Closes#9512

Remove write barriers for fields of ref ByrefLikeStruct arguments since we know that such refs always point to stack, Example:

voidTest(refMyStructs,objecto1,objecto2){s.A=o1;s.B=o2;}refstructMyStruct{publicobjectA;publicobjectB;}

Codegen for Test(...) on Main:

; Method Prog:Test(byref,System.Object,System.Object):this (FullOpts)G_M1451_IG01:pushrsipushrbxmovrbx,rdxmovrsi,r9G_M1451_IG02:movrcx,rbxmovrdx,r8call CORINFO_HELP_CHECKED_ASSIGN_REFlearcx, bword ptr [rbx+0x08]movrdx,rsicall CORINFO_HELP_CHECKED_ASSIGN_REFnopG_M1451_IG03:poprbxpoprsiret; Total bytes of code: 35

This PR:

; Method Prog:Test(byref,System.Object,System.Object):this (FullOpts)G_M1451_IG01:G_M1451_IG02:mov gword ptr [rdx],r8mov gword ptr [rdx+0x08],r9G_M1451_IG03:ret; Total bytes of code: 8

Similar case when ref struct is "this":

refstructRefLike{publicobjecto;publicvoidTest(objecto){this.o=o;// Main: checked-write-barrier, PR: no-barrier}}

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 15, 2024
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@MihuBot

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot

usingBenchmarkDotNet.Attributes;usingSystem.Runtime.CompilerServices;publicclassMyBench{[Benchmark]publicvoidBench(){MyStructs=default;Test(refs,null,null);}[MethodImpl(MethodImplOptions.NoInlining)]voidTest(refMyStructs,objecto1,objecto2){s.A=o1;s.B=o2;}refstructMyStruct{publicobjectA;publicobjectB;}}

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-YFOKOK : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-EVRGON : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainMeanErrorRatio
BenchMain4.631 ns0.0076 ns1.00
BenchPR1.720 ns0.0002 ns0.37

BDN_Artifacts.zip

@hez2010

hez2010 commented Jun 15, 2024

Copy link
Copy Markdown
Contributor

I think we can also do this for objects allocated on the stack due to escape analysis.

@EgorBo

EgorBo commented Jun 15, 2024

Copy link
Copy Markdown
MemberAuthor

I think this can also be done for objects allocated on the stack due to escape analysis.

We already do that where it's legal afair. Although, in some cases we have to switch to slower "checked" write barriers when "object on stack" feature is enabled since TYP_REF no longer gives the "always on heap" guarantees.

@EgorBo
EgorBo marked this pull request as ready for review June 15, 2024 09:11
@EgorBo

EgorBo commented Jun 15, 2024

Copy link
Copy Markdown
MemberAuthor

PTAL @jakobbotsch@AndyAyersMS cc @dotnet/jit-contrib (CI failures are unrelated). Diffs

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -arm64 -profiler

usingBenchmarkDotNet.Attributes;usingSystem.Runtime.CompilerServices;publicclassMyBench{[Benchmark]publicvoidBench(){MyStructs=default;Test(refs,null,null);}[MethodImpl(MethodImplOptions.NoInlining)]voidTest(refMyStructs,objecto1,objecto2){s.A=o1;s.B=o2;}refstructMyStruct{publicobjectA;publicobjectB;}}

@EgorBot

Copy link
Copy Markdown
Benchmark results on Arm64
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Unknown processor
Job-IRHBKJ : .NET 9.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
Job-UINBZM : .NET 9.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
MethodToolchainMeanErrorRatio
BenchMain3.982 ns0.0029 ns1.00
BenchPR2.696 ns0.0425 ns0.68

BDN_Artifacts.zip

Flame graphs: Main vs PR 🔥
Hot asm: Main vs PR
Hot functions: Main vs PR

For clean perf results, make sure you have just one [Benchmark] in your app.

Comment threadsrc/coreclr/inc/cordebuginfo.h Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated

if (info.compMethodInfo->args.hasImplicitThis() && varNum == info.compThisArg)
{
// implicit this doesn't map to any IL arg

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.

Suggested change
// implicit this doesn't map to any IL arg
// implicit this doesn't map to any method signature arg

assuming this change is otherwise correct that I am not sure about

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I reverted those changes since compMap2ILvarNum is also used in debug info generation and I don't want to break it accidentally. I put the workaround directly to my function.

@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.

This change has the same change in our aliasing model as #97997 that we rejected did (relevant comment: #97997 (comment)).

Basically, before this change, it is ok in our aliasing model to write code like this:

structA{publicobjectX;}refstructB{publicobjectX;}voidFoo(B*b){((A*)b)->X="abc";}voidBar(A*a){Foo((B*)a);}

The optimization made by this PR is only valid if we consider this kind of aliasing to be UB. I am fine with doing that if the diffs justify it, but it is something to be aware about and explicit around if we take this change.

Comment threadsrc/coreclr/jit/assertionprop.cpp Outdated
Comment on lines +5059 to +5124
// Check if we deal with `ref ByrefLikeStruct` argument
unsigned lclNum = (unsigned)vnStore->CoercedConstantValue<ssize_t>(funcApp.m_args[0]);
assert(lclNum != BAD_VAR_NUM);
if (comp->lvaIsParameter(lclNum))
{
if (comp->info.compThisArg == lclNum)
{
// No barrier needed if current class is byref-like
// and the destination is an implicit "this"
return comp->eeIsByrefLike(comp->info.compClassHnd) ? GCInfo::WriteBarrierForm::WBF_NoBarrier
: GCInfo::WriteBarrierForm::WBF_BarrierUnknown;
}

// Find the corresponding argument index. Basically, we need to ignore the hidden args
// such as generic context, return buffer, etc..
unsigned ilArg = comp->compMap2ILvarNum(lclNum);
switch (static_cast<int>(ilArg))
{
// Ignore non-user args
case ICorDebugInfo::RETBUF_ILNUM:
case ICorDebugInfo::VARARGS_HND_ILNUM:
case ICorDebugInfo::TYPECTXT_ILNUM:
case ICorDebugInfo::UNKNOWN_ILNUM:
return GCInfo::WriteBarrierForm::WBF_BarrierUnknown;

default:
break;
}
assert(ilArg != BAD_VAR_NUM);

// A small inconsistency: various signature walking VM APIs (e.g. getArgClass)
// do not expose implicit "this" args, while compMap2ILvarNum does.
if (comp->info.compMethodInfo->args.hasImplicitThis())
{
assert(ilArg > 0);
ilArg--;
}

// First, get corresponding CORINFO_ARG_LIST_HANDLE arg data
CORINFO_ARG_LIST_HANDLE currentArg = comp->info.compMethodInfo->args.args;
for (unsigned i = 0; i < ilArg; i++)
{
currentArg = comp->info.compCompHnd->getArgNext(currentArg);
}

// Now get its type and strip `ref` so we can get CORINFO_CLASS_HANDLE of the
// underlying type
CORINFO_CLASS_HANDLE byrefCls = comp->eeGetArgClass(&comp->info.compMethodInfo->args, currentArg);
if (byrefCls != NO_CLASS_HANDLE)
{
CORINFO_CLASS_HANDLE actualCls;
CorInfoType actualClsType = comp->info.compCompHnd->getChildType(byrefCls, &actualCls);

// Only structs can be byref-like
if (actualClsType == CORINFO_TYPE_VALUECLASS)
{
assert(actualCls != NO_CLASS_HANDLE);
if (comp->eeIsByrefLike(actualCls))
{
// It's byref-like, barrier is not needed
return GCInfo::WriteBarrierForm::WBF_NoBarrier;
}
}
}
}
return GCInfo::WriteBarrierForm::WBF_BarrierUnknown;

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 look potentially quite expensive. You may want to try running an SPMI collection on this PR to get more accurate diffs, there are a lot of misses.

Comment threadsrc/coreclr/jit/assertionprop.cpp Outdated
Comment on lines +5104 to +5107
// Now get its type and strip `ref` so we can get CORINFO_CLASS_HANDLE of the
// underlying type
CORINFO_CLASS_HANDLE byrefCls = comp->eeGetArgClass(&comp->info.compMethodInfo->args, currentArg);
if (byrefCls != NO_CLASS_HANDLE)

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.

What is actually checking that this is a byref to that type? Would this potentially pass for an array as well?

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.

The entire code sequence is a bit unfortunate... it would be much cleaner/less scary looking if we had just saved enough information in LclVarDsc (or a side table, similar to lvaParameterPassingInfo) to get the full class handle for the parameter. I suppose the main problem around that is that we don't actually call getArgClass on all parameters today, only on TYP_REF parameters.

I wonder if we should just bite the bullet and call getArgClass on all parameters, or perhaps change getArgType to unconditionally return a class handle (today it only returns the class handle for value classes).

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.

Agree -- we should compute this early when we first process the method and local sigs and cache it on the local var.

@jkotas

Copy link
Copy Markdown
Member

The optimization made by this PR is only valid if we consider this kind of aliasing to be UB.

I think it is fine to make this form of aliasing UB, in particular when it involves managed types.

@EgorBo

EgorBo commented Jun 16, 2024

Copy link
Copy Markdown
MemberAuthor

if the diffs justify it

Current diffs are not too big, but I am wondering if we can, sort of, mark more internal structs as stack-only where it's allowed - I already tried that for CoreLib and found 50 structs eligable for it.

Also, perhaps, e.g. NativeAOT can do this as part of IL analysis as an optimization

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@MihuBot

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -profiler

usingBenchmarkDotNet.Attributes;usingSystem.Runtime.CompilerServices;publicclassMyBench{[Benchmark]publicvoidBench(){MyStructs=default;Test(refs,null,null);}[MethodImpl(MethodImplOptions.NoInlining)]voidTest(refMyStructs,objecto1,objecto2){s.A=o1;s.B=o2;}refstructMyStruct{publicobjectA;publicobjectB;}}

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-VSVRXS : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-CUMSFN : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainMeanErrorRatio
BenchMain4.366 ns0.0080 ns1.00
BenchPR1.437 ns0.0006 ns0.33

BDN_Artifacts.zip

Flame graphs: Main vs PR 🔥
Hot asm: Main vs PR
Hot functions: Main vs PR

For clean perf results, make sure you have just one [Benchmark] in your app.

@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.

We might also be able to make the conservative VNs for these structs more aggressive (they can match the liberal vns), since they can't be modified cross-thread.

Comment threadsrc/coreclr/jit/assertionprop.cpp Outdated
Comment on lines +5104 to +5107
// Now get its type and strip `ref` so we can get CORINFO_CLASS_HANDLE of the
// underlying type
CORINFO_CLASS_HANDLE byrefCls = comp->eeGetArgClass(&comp->info.compMethodInfo->args, currentArg);
if (byrefCls != NO_CLASS_HANDLE)

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.

Agree -- we should compute this early when we first process the method and local sigs and cache it on the local var.

@EgorBo

EgorBo commented Jun 26, 2024

Copy link
Copy Markdown
MemberAuthor

@jakobbotsch@AndyAyersMS can you take a look again? I ended up reserving a bitfield in LclVarDesc

win-x64 Release layout
4>class LclVarDsc	size(72):
4>	+---
4> 0.	| var_types lvType (bitstart=0,nbits=5)
4> 0.	| lvIsParam (bitstart=5,nbits=1)
4> 0.	| lvIsRegArg (bitstart=6,nbits=1)
4> 0.	| lvFramePointerBased (bitstart=7,nbits=1)
4> 1.	| lvOnFrame (bitstart=0,nbits=1)
4> 1.	| lvRegister (bitstart=1,nbits=1)
4> 1.	| lvTracked (bitstart=2,nbits=1)
4> 1.	| lvPinned (bitstart=3,nbits=1)
4> 1.	| lvMustInit (bitstart=4,nbits=1)
4> 1.	| m_addrExposed (bitstart=5,nbits=1)
4> 1.	| lvDoNotEnregister (bitstart=6,nbits=1)
4> 1.	| lvFieldAccessed (bitstart=7,nbits=1)
4> 2.	| lvLiveInOutOfHndlr (bitstart=0,nbits=1)
4> 2.	| lvInSsa (bitstart=1,nbits=1)
4> 2.	| lvIsCSE (bitstart=2,nbits=1)
4> 2.	| lvHasLdAddrOp (bitstart=3,nbits=1)
4> 2.	| lvHasILStoreOp (bitstart=4,nbits=1)
4> 2.	| lvHasMultipleILStoreOp (bitstart=5,nbits=1)
4> 2.	| lvIsTemp (bitstart=6,nbits=1)
4> 2.	| lvIsImplicitByRef (bitstart=7,nbits=1)
4> 3.	| lvIsLastUseCopyOmissionCandidate (bitstart=0,nbits=1)
4> 3.	| lvSingleDef (bitstart=1,nbits=1)
4> 3.	| lvSingleDefRegCandidate (bitstart=2,nbits=1)
4> 3.	| lvDisqualifySingleDefRegCandidate (bitstart=3,nbits=1)
4> 3.	| lvSpillAtSingleDef (bitstart=4,nbits=1)
4> 3.	| lvHasExceptionalUsesHint (bitstart=5,nbits=1)
4> 3.	| lvQuirkToLong (bitstart=6,nbits=1)
4> 3.	| lvIsPtr (bitstart=7,nbits=1)
4> 4.	| lvIsUnsafeBuffer (bitstart=0,nbits=1)
4> 4.	| lvPromoted (bitstart=1,nbits=1)
4> 4.	| lvIsStructField (bitstart=2,nbits=1)
4> 4.	| lvContainsHoles (bitstart=3,nbits=1)
4> 4.	| lvAnySignificantPadding (bitstart=4,nbits=1)
4> 4.	| lvIsMultiRegArg (bitstart=5,nbits=1)
4> 4.	| lvIsMultiRegRet (bitstart=6,nbits=1)
4> 4.	| lvLRACandidate (bitstart=7,nbits=1)
4> 5.	| lvUsedInSIMDIntrinsic (bitstart=0,nbits=1)
4> 5.	| lvRegStruct (bitstart=1,nbits=1)
4> 5.	| lvClassIsExact (bitstart=2,nbits=1)
4> 5.	| lvImplicitlyReferenced (bitstart=3,nbits=1)
4> 5.	| lvSuppressedZeroInit (bitstart=4,nbits=1)
4> 5.	| lvHasExplicitInit (bitstart=5,nbits=1)
4> 5.	| lvIsOSRLocal (bitstart=6,nbits=1)
4> 5.	| lvIsOSRExposedLocal (bitstart=7,nbits=1)
4> 6.	| lvRedefinedInEmbeddedStatement (bitstart=0,nbits=1)
4> 6.	| lvIsNeverNegative (bitstart=1,nbits=1)
4> 6.	| lvIsSpan (bitstart=2,nbits=1)
4> | <alignment member> (size=1) <-------------------- HERE!
4> 8	| lvFieldLclStart
4> 8	| lvParentLcl
4>12	| lvFieldCnt
4>13	| lvFldOffset
4>14	| lvFldOrdinal
4>15.	| lvAllDefsAreNoGc (bitstart=0,nbits=1)
4>16	| _lvRegNum
4>17	| _lvArgReg
4>18	| _lvArgInitReg
4> | <alignment member> (size=1)
4>20	| lvVarIndex
4>22	| m_lvRefCnt
4>24	| m_lvRefCntWtd
4>32	| lvStkOffs
4>36	| lvSlotNum
4>40	| lvClassHnd
4>48	| m_layout
4>56	| ?$SsaDefArray@VLclSsaVarDsc@@ lvPerSsaData
4>	+---

I also considered re-using lvClassHnd or m_Layout fields for it, but that required a bit more changes. Same Diffs with many missing contexts (jit-diff is the same).
I was thinking to play with "interproc" analysis and mark more structs as "never leave stack" to get bigger diffs (there are many cases), but will do it separately

Comment threadsrc/coreclr/vm/jitinterface.cpp Outdated
Comment on lines +9427 to +9429
case ELEMENT_TYPE_BYREF:
typeHnd = ptr.GetTypeHandleThrowing(pModule, &typeContext);
break;

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 we do it for all types? If not, can we at least do it for ELEMENT_TYPE_PTR as well?

The documentation of the JIT-EE API should be updated (vcTypeRet should probably be renamed too). Also, what about crossgen2/NAOT implementations of this API?

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.

Does doing this cause the operand type of the byref/pointer to be loaded eagerly?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Also, what about crossgen2/NAOT implementations of this API?

They seem to already always return it, so my test snippet is optimized there too

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Does doing this cause the operand type of the byref/pointer to be loaded eagerly?

How do I check that? I presume since it's a parameter it's going to be loaded anyway?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Can we do it for all types?

Any particular reason to do that? Isn't, like you mentioned, it might lead to redundant type load events we don't need?

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.

How do I check that? I presume since it's a parameter it's going to be loaded anyway?

Maybe @jkotas can answer this question?

Any particular reason to do that? Isn't, like you mentioned, it might lead to redundant type load events we don't need?

The reason would be to make the JIT-EE API more regular in its contract, and avoid us having to call back to the EE side in the other cases too (e.g. we want this information for class types as well, and we currently do another JIT-EE call to get it). We could remove getArgClass entirely if getArgType just returned the class handle always.

@jkotasjkotasJun 26, 2024

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.

it might lead to redundant type load events we don't need?

Once we load the type, the future attempts to load the type are cheap. There is not such a thing as "redundant type load events".

The point is to avoid loading the type in the first place. As you can see in the ELEMENT_TYPE_PTR comment below, eagerly loading types that do not need to be loaded can even break stuff. Avoiding unnecessary type loading is goodness for startup.

I think the JIT should never need to load the unmanaged pointer types. It should treat all unmanaged pointers as opaque void*.

This optimization should be based on the type provided in the stfld metadata token.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This optimization should be based on the type provided in the stfld metadata token.

Actually, this might simplify changes a lot, let me try

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 optimization should be based on the type provided in the stfld metadata token.

This would also be much more in line with how our normal modelling of aliasing and stores/loads work and effectively would mean that we aren't changing the aliasing model, so if we can do that it seems like it would be much cleaner overall.

Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
CORINFO_CLASS_HANDLE clsHnd = info.compCompHnd->getArgClass(&info.compMethodInfo->args, argLst);
lvaSetClass(varDscInfo->varNum, clsHnd);
}
else if (type == CORINFO_TYPE_BYREF)

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.

Do we already avoid the write barrier if this was an unmanaged pointer?

@EgorBoEgorBoJun 26, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you mean something like this:

unsafevoidTest(string*a,stringb){*a=b;}

isn't it not safe to remove WB here? And, hopefully, nobody does this anyway 🙂
Currenly it does emit a checked WB.

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.

Yes, it would be unsafe to remove the write barrier there.

I think it means we should make this feature work for CORINFO_TYPE_PTR too, for parity between managed and unmanaged pointers. E.g.

 [MethodImpl(MethodImplOptions.NoInlining)]
void Test(MyStruct* s, object o1, object o2)
{
s->A = o1;
s->B = o2;
}
ref struct MyStruct
{
public object A;
public object B;
}

should avoid the write barrier as well, like if it was a managed pointer.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

NOTE that MyStruct* s is issueing a warning in Roslyn as an attempt to take an address of a managed type. Do we want to also eagerly load all types under unmanaged pointers in hope somebody do this (I presume it's rare/never).

cc @jkotas

@jkotasjkotasJun 26, 2024

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.

I think it is fine to assume that both ref MyByRefLikeStruct and MyByRefLikeStruct* never point into GC heap here.

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.

What I'm after is that we have consistency in optimizations like these. Switching a byref to a pointer because the target is on the stack should not come with surprising perf regressions like new write barriers.

@EgorBo
EgorBoforce-pushed the optimize-byref-structs branch from 5436aba to ace3a9dCompareJune 26, 2024 15:04
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@MihuBot

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@MihuBot

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Thanks for reviews and the suggestion, stsfld really eliminated all the complexity, no idea why I never thought about it 🙂

@EgorBo
EgorBo merged commit 520af01 into dotnet:mainJun 27, 2024
@EgorBo
EgorBo deleted the optimize-byref-structs branch June 27, 2024 19:27
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 28, 2024
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.

Optimize out write barriers for fields in ref-like structs

6 participants

@EgorBo@EgorBot@hez2010@jkotas@jakobbotsch@AndyAyersMS