-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JIT: Mark some intrinsics as must-expand for NAOT due to ILScanner dependencies #109609
JIT: Mark some intrinsics as must-expand for NAOT due to ILScanner dependencies #109609
Conversation
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
@@ -3397,6 +3397,13 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd, | |||
betterToExpand = true; | |||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not totally clear to me whether we want these to be mustExpand
or not, given the "If the intrinsic cannot possibly be expanded, it's fine" in the comment above... usually that would mean that from the JIT's perspective, we would not want to do the expansion in MinOpts.
(In the end there is no practical difference in behavior... today we expand betterToExpand
intrinsics even in MinOpts due to a check below, but that sort of goes against our general MinOpts philosophy, I think.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RuntimeHelpers_CreateSpan and RuntimeHelpers_InitializeArray are pattern matched. If they're not part of the specific IL patter, RyuJIT cannot possibly expand them - that's why it's not mandatory, just "whenever it's in the right shape". We don't want e.g. RuntimeHelpers.InitializeArray(null, default);
to fail to compile. But if these intrinsics are not expanded by RyuJIT their method bodies just throw PNSE, we don't have an implementation, so we want to expand everything humanely possible.
The rest are basically mustExpand, we don't have meaningful bodies for them. Could achieve the same thing by making them recursive? It has been a long time since I was looking at this and at that time we didn't have mustExpand/betterToExpand distinction or the recursive special handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could achieve the same thing by making them recursive?
Good point. GetArrayDataReference is defined recursively
Lines 24 to 25 in bad7318
public static ref T GetArrayDataReference<T>(T[] array) => | |
ref GetArrayDataReference(array); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, this only handles the actual recursive call inside the method. Ignore my comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mustExpand
exists to flag cases that are required to be handled for correctness reasons. This should largely only apply to recursive calls (to avoid the stack overflow). In general it should never be incorrect to leave a call in because things like reflection, indirect invocation (delegates or function pointers), debugger evaluation, or other cases need to work.
betterToExpand
then exists to flag cases where it is profitable to do the expansion in MinOpts
. This is primarily relevant to things that expand to trivial constant and which are likely to allow dead-code elimination or other simple transforms we do for similar reasons. Such cases most typically improve throughput of the JIT and are highly unlikely to cause bugs.
We notably also treat some other cases, like hardware intrinsics, as functionally being betterToExpand
(but we handle them explicitly early on, not via this local). We do it for these because they are known to be typically perf critical, are likely in methods that will be hot and be tiered, compile down to (most typically) singular instructions, generally improve JIT throughput/reduce JIT memory overhead as the hwintrinsic node is much cheaper than a call node (smaller, needs less handling in typical cases, doesn't require ABI fixups, doesn't need additional spills or copies to be introduced, etc), and because the reduce the overall overhead of the app; that is we don't free code for methods that were compiled to T0 and later rejitted; the overhead of function alignment + vzeroupper + instruction + ret
for dozens to hundreds of intrinsics is non-trivial, as is the overhead required to meet ABI requirements per call (typically involving spilling the inputs so they can be passed by ref, allocating a return buffer, handling register save/restore requirements for V256/V512 registers, etc), and there often being many tightly coupled such calls in any method using them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general it should never be incorrect to leave a call in because things like reflection
It is required for correctness in this specific case. IL scanner assumes that these specific methods are always going to be expanded to save binary size related to generic dictionaries. These methods are very popular, so the size is disproportionate compared to what these methods do. If we were to remove the hardcoded assumption from IL scanner, we would take a binary size hit for native AOT.
This will be cleaner if/once IL Scanner is replaced by actual JIT importer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
betterToExpand
then exists to flag cases where it is profitable to do the expansion inMinOpts
. This is primarily relevant to things that expand to trivial constant and which are likely to allow dead-code elimination or other simple transforms we do for similar reasons. Such cases most typically improve throughput of the JIT and are highly unlikely to cause bugs.
For me there is a distinction between tier0 and MinOpts for these things. Tier0 implies MinOpts, and for Tier0 we want to do the cheap and profitable optimizations when possible, but for MinOpts without Tier0 my philosophy is that we truly do not want to do any transformations if we can avoid it (generally for diagnostic/debugging/testing reasons).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rest are basically mustExpand, we don't have meaningful bodies for them. Could achieve the same thing by making them recursive?
I moved the rest of them to the mustExpand
section as well. As pointed out above the recursiveness only matters when compiling the intrinsic method itself, not for callers of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but for MinOpts without Tier0 my philosophy is that we truly do not want to do any transformations if we can avoid it (generally for diagnostic/debugging/testing reasons).
I generally agree, which is also why I had the annotation above:
In general it should never be incorrect to leave a call in because things like reflection, indirect invocation (delegates or function pointers), debugger evaluation, or other cases need to work.
In this case it sounds like its special and we have to expand it for NAOT for other reasons as well, so I don't think there's much we can do.
I don't think we'd have a problem with ignoring betterToExpand
for true MinOpts
(where we're falling back from some compilation failure or similar and just trying to emit functioning code). But, there is an incredibly high cost to this for methods using platform specific hardware intrinsics. Things like the ability to step into a method in the debugger similarly don't matter for them, as they are purely recursive calls, so it might be worth considering such cases to be functionally equivalent to IL opcodes and expanded anyways (and if IL were designed today, its very possible that SIMD would have a more direct representation).
@MichalStrehovsky I'm assuming this test failure is harmless, and not indicative of a bug?
Similarly it looks like there's some failures in size-validating tests, which is to be expected with the forced MinOpts. |
Yes, this test is testing that size optimizations kick in and those rely on RyuJIT doing stuff. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (with forced minOpts deleted)
Is this safe for the other overload which is not always expanded? |
Good point. I think it will work fine, but it should not be strictly required to mustExpand the non-generic variant. |
This reverts commit 6db6378.
Looked through the failures in smoketests and saw only the ones we discussed above. I'll try a run of nativeaot-outerloop in #103950 once this one is merged. |
These two intrinsics are treated specially by ILScanner, and thus must always be expanded for NAOT, even in MinOpts.
Context: #103950 (comment)