JIT: optimize Enum.HasFlag#13748
Conversation
|
@jkotas PTAL (vm parts at least) No jit-diffs impact,. There was one use of HasFlag in the test suite in baseservices\threading\commitstackonlyasneeded\DefaultStackCommit and the optimization fires on it when jitting: ;; Before
48B980B284EAFD7F0000 mov rcx, 0x7FFDEA84B280
E81A07865F call CORINFO_HELP_NEWSFAST
4C8BF8 mov r15, rax
8B8C2480000000 mov ecx, dword ptr [rsp+80H]
41894F08 mov dword ptr [r15+8], ecx
48B980B284EAFD7F0000 mov rcx, 0x7FFDEA84B280
E8FD06865F call CORINFO_HELP_NEWSFAST
488BD0 mov rdx, rax
C7420800100000 mov dword ptr [rdx+8], 0x1000
498BCF mov rcx, r15
E85B2DA75E call System.Enum:HasFlag(ref):bool:this
85C0 test eax, eax
0F857F000000 jne G_M54767_IG12
;; After
8B8C2480000000 mov ecx, dword ptr [rsp+80H]
F7C100100000 test ecx, 0x1000
754C jne SHORT G_M54767_IG12Will do some corefx testing too since there is likely better coverage there. @dotnet-bot test Windows_NT x64 corefx_baseline |
JosephTremoulet
left a comment
There was a problem hiding this comment.
Great to see this getting implemented, thanks!
| GenTreePtr gtFoldExprCompare(GenTreePtr tree); | ||
| bool gtTryRemoveBoxUpstreamEffects(GenTreePtr tree); | ||
|
|
||
| enum BoxRemovalOptions |
| GenTree* asgStmt = op->gtBox.gtAsgStmtWhenInlinedBoxValue; | ||
| assert(asgStmt->gtOper == GT_STMT); | ||
| GenTreePtr copyStmt = op->gtBox.gtCopyStmtWhenInlinedBoxValue; | ||
| GenTree* copyStmt = op->gtBox.gtCopyStmtWhenInlinedBoxValue; |
There was a problem hiding this comment.
Decided not to introduce new GenTreePtr and then having a mixture looked odd. So changed them all.
|
|
||
| // Pop the stack for real. | ||
| // impPopStack(); | ||
| // impPopStack(); |
There was a problem hiding this comment.
Looks like you do this in the importer now when calling from the importer, so can remove this commented code.
There was a problem hiding this comment.
Thanks, yeah a vestige of the initial importer-only version.
| return nullptr; | ||
| } | ||
|
|
||
| GenTree* flagVal = gtTryRemoveBoxUpstreamEffects(flagOp, BR_DONT_REMOVE); |
There was a problem hiding this comment.
I'd be inclined to go ahead and use BR_REMOVE_BUT_NOT_NARROW here (and skip the call below), despite the asymmetry.
There was a problem hiding this comment.
Good idea. Originally there was one more downstream check so both removals needed to be trials.
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| // Some simple tests for the Enum.HasFlag optimization. | ||
| // All the calls to HasFlag should be optimized to simple compares. |
There was a problem hiding this comment.
And are they all optimized with this code, or is there more to do for some of them?
There was a problem hiding this comment.
All are now optimized (initially the ones with calls weren't). I'll fix the comment to be clearer.
| // The thisVal and flagVal trees come from earlier statements. We need | ||
| // to evaluate them both to temps at those points to safely transmit | ||
| // the values here. | ||
| const unsigned flagTmp = lvaGrabTemp(true DEBUGARG("Enum:HasFlag flag temp")); |
There was a problem hiding this comment.
I guess it's not worth the trouble of handling the common case where flagVal is a constant and doesn't require a new temp.
There was a problem hiding this comment.
I think it may be worth it... will give it a try.
| { | ||
| printf("\nAttempting to remove side effects of BOX (valuetype)\n"); | ||
| printf("\n%s remove side effects of BOX (valuetype)\n", | ||
| options == BR_DONT_REMOVE ? "Checking if it is possible" : "Attempting to"); |
There was a problem hiding this comment.
Nit: either move the "to " from "Attempting to" to before "remove" , or add after "possible".
|
Pushed an update (but missed Carol's note). Now in cases like public static int Main()
{
if (E.RED.HasFlag(E.BLUE))
{
Console.WriteLine("This bit should not be imported!\n");
}
return 100;
}The test is folded in the importer. |
|
Making sure this still passes: @dotnet-bot test Windows_NT x64 corefx_baseline |
|
Oops, broke the test case; now fixed. Also fixed explanatory message per Carol. Making sure this still passes: @dotnet-bot test Windows_NT x64 corefx_baseline |
| // Unless they are invariant values, we need to evaluate them both | ||
| // to temps at those points to safely transmit the values here. | ||
| // | ||
| // Also we need ot use the flag twice, so we need two trees for it. |
There was a problem hiding this comment.
Found one other typo; will fix them both.
|
The type equivalence implementation needs to be more careful about inexact types in shared generic code. The CoreCLR type system is not capable of capturing the exact type information for shared generic code (ie it does not have equivalent of RuntimeDeterminedType from CoreRT type system).
It may be nice to plumb this such that these optimization can kick in shared generic code when the EE side has exact types in shared generic code. |
|
@jkotas thanks for pointing this out. Think I know the right way to fix it... |
| } | ||
| } | ||
| else if (pMT->HasSameTypeDefAs(MscorlibBinder::GetClass(CLASS__READONLY_SPAN))) | ||
| else if (pMT->HasSameTypeDefAs(MscorlibBinder::GetClass(CLASS__ENUM))) |
There was a problem hiding this comment.
The preferred way to define non-generic intrinsincs is to make them FCall and use FCIntrinsic to get them ID assigned in vm\ecalllist.h. The advantage of it is that the intrinsic ID is cached and the scheme scales well with the number of intrinsics.
For Enum.HasFlag, it would require moving the bit of error handling from C# to C++ so that the main entrypoint is FCall.
There was a problem hiding this comment.
So basically move the checking in HasFlag down into InternalHasFlag, then remove the former and rename the latter as HasFlag?
There was a problem hiding this comment.
Looked into this some -- it is not obvious how to create the proper exception with formatted text for the class equivalence check. Any suggestions?
|
Otherwise LGTM. |
|
Looks like it needs some fixes for non-default underlying types too. |
|
@jkotas what is the explanation for the R4/R8 support in Enum.cs, for instance in |
|
It may be nice to add comment about this at the top of Enum.cs. It is frequently asked question. |
|
Current thinking is to revise how @jkotas any advice on how to create the properly formatted type mismatch exception on the VM side? It seems to me like it will take a fair amount of extra plumbing... |
I think that the easiest way to do it is to call back to managed code to throw the exception. Maybe we should use this as an opportunity to create a protocol that allows arbitrary (non-FCall) CoreLib methods to be recognized as JIT intrinsic. Such scheme will be needed for SIMD intrinsics that are being worked on as well. |
|
Here's a fork with my take on a fully native Crafting a more flexible mechanism for determining which methods can be jit intrinsics sounds like a good idea, though I am not clear on what all would be required. |
|
|
@jkotas here's a first cut at the new style intrinsic support. Let me know if you have a better name for these new kind of intrinsics and/or there is a better place to check for the attribute. If this looks plausible I'll put it up for PR. |
|
Yes, it looks plausible - I have left a few comments on the commit. |
I would expect that we will migrate all JIT intrinsics over to this scheme over time, so I would not worry about the name too much. |
Check for calls to `Enum.HasFlag` using the new named intrinsic support introduced in dotnet#13815. Implement a simple recognizer for these named intrinsics (currently just recognizing `Enum.HasFlag`). When the call is recognized, optimize if both operands are boxes with compatible types and both boxes can be removed. The optimization changes the call to a simple and/compare tree on the underlying enum values. To accomplish this, generalize the behavior of `gtTryRemoveBoxUpstreamEffects` to add a "trial removal" mode and to optionally suppress narrowing of the copy source. Invoke the optimization during importation (which will catch most cases) and again during morph (to get the post-inline cases). Added test cases. Suprisingly there were almost no uses of HasFlag in the current CoreCLR test suite. Closes #5626.
e3e7322 to
9d0479e
Compare
|
Reworked this now that the new intrinsic support is in place. Most of the code is as before, but the recognition bit is different. I put in some toeholds for a more general classification framework for the new intrinsics; eventually we can flesh this out with more information as we need it. @JosephTremoulet PTAL |
|
@dotnet-bot retest Ubuntu arm64 Cross Debug Build |
| // | ||
| // False if the upstream effects could not be removed. | ||
| // A tree representing the original value to box, if removal | ||
| // is successful (but see note). nullptr if removal fails. |
There was a problem hiding this comment.
FWIW, after reading this and the note I was still not sure what the return value is when BR_DONT_REMOVE gets passed... something like successful/possible instead of just successful here, or an explicit Still returns the source tree if removal is legal in the note for BR_DONT_REMOVE, would maybe have helped.
JosephTremoulet
left a comment
There was a problem hiding this comment.
Looks good, just some very minor observations.
| } | ||
|
|
||
| // A boxed flagOp should have exact type and non-null instance | ||
| assert(isExactFlag && isNonNullFlag); |
There was a problem hiding this comment.
Nit: I always prefer assert(x); assert(y); over assert(x && y), so you know which failed when one fails...
| bool isNonNullFlag = false; | ||
| CORINFO_CLASS_HANDLE flagHnd = gtGetClassHandle(flagOp, &isExactFlag, &isNonNullFlag); | ||
|
|
||
| if (flagHnd == nullptr) |
There was a problem hiding this comment.
You don't have the CORINFO_FLG_SHAREDINST check for the flag operand. I presume it couldn't equal thisHnd if it were shared... could consider adding a comment, or deferring the shared inst check until after the handles are checked for equality, just for readability.
There was a problem hiding this comment.
Deferring the check.
|
OSX test failure is unrelated: faililng pal test |
|
@dotnet-bot retest OSX10.12 x64 Checked Build and Test |
|
Tizen time out is worrisome, but the same changes passed before. Am going to retry. @dotnet-bot retest Tizen armel Cross Debug Build |
In the vm, add mscorlib binder support for Enum.HasFlag. Recognize this
method as a may-expand intrinsic.
In the jit, generalize the behavior of gtTryRemoveBoxUpstreamEffects to
add a "trial removal" mode and to optionally suppress narrowing of the
copy source.
Then implement the optimization by checking for the intrinsic and ensuring
that both operands are boxes with compatible types that can be removed.
The optimization changes the call to a simple and/compare tree on the
underlying enum values.
Invoke the optimzation during importation (which will catch most cases) and
again during morph (to get the post-inline cases).
Currently not thinking of bumping the JIT GUID since this is a may expand
intrinsic and no existing intrinsic ID has changed.
Added test case. Suprisingly there were almost no uses of HasFlag in the
current CoreCLR test suite.
Closes #5626.