Enable checking of GTF_EXCEPT and GTF_ASG flags.#13668
Conversation
fgDebugCheckFlags is modified to check that GTF_EXCEPT and GTF_ASG are set precisely when needed. It's also modified to handle several special operators correctly. fgAddrCouldBeNull is updated to check for handles, implicit byref locals, and stack byrefs. OperMayThrow is modified to handle several operators correctly. GTF_IND_NONFAULTING is reused on operations for which OperIsIndir() is true and on GT_ARR_LENGTH. Various places in morph are updated to set side effect flags correctly. gtUpdateSideEffects is re-written so that it's precise for GTF_ASG and GTF_EXCEPT and conservatively correct for the other side effects. It's now called from more places to keep the flags up-to-date after transformations. NoThrow in HelperCallProperties is updated and GTF_EXCEPT flag is set on helper calls according to that property. optRemoveRangeCheck is cleaned up and simplified.
|
SPC codesize numbers: |
|
Benchmarks codesize numbers: |
|
Frameworks codesize numbers: |
|
I measured instructions retired TP impact when crossgen-ing SPC: |
|
@pgavlin @JosephTremoulet @briansull @CarolEidt @dotnet/jit-contrib PTAL |
|
The three main sources of regressions:
|
|
@dotnet-bot test Ubuntu16.04 arm Cross Debug Build |
CarolEidt
left a comment
There was a problem hiding this comment.
Looks good overall. I have a few suggestions.
|
|
||
| inline GenTreeCall* Compiler::gtNewHelperCallNode(unsigned helper, var_types type, unsigned flags, GenTreeArgList* args) | ||
| //------------------------------------------------------------------------------ | ||
| // gtNewHelperCallNode : Helper to create a call helper node. |
There was a problem hiding this comment.
Suggestion: I would simply say "Create a node that calls a helper method.". The repetition of "helper" is somewhat confusing, and although a number of these gtNew methods are described as helpers, I'm not sure I'd really call them that.
| unsigned mask = GTF_COMMON_MASK; | ||
| if (this->OperIsIndirOrArrLength() && OperIsIndir(oper)) | ||
| { | ||
| mask |= GTF_IND_NONFAULTING; |
There was a problem hiding this comment.
Suggestion: at first glance this is confusing because it almost looks like you're unconditionally setting this flag (though if one reads a bit further it's clear). It wouldn't hurt to add a comment such as "If this is an indirection, we want to inherit the value of the GTF_IND_NONFAULTING flag in addition to anything set in GTF_COMMON_MASK or something to that effect.
| @@ -1529,8 +1601,13 @@ inline void GenTree::ChangeOper(genTreeOps oper, ValueNumberUpdate vnUpdate) | |||
|
|
|||
| inline void GenTree::ChangeOperUnchecked(genTreeOps oper) | |||
There was a problem hiding this comment.
Would it make sense for ChangeOper to call this, so that the logic (such as it is) doesn't have to be duplicated?
There was a problem hiding this comment.
ChangeOper calls SetOper while ChangeOperUnchecked calls SetOperRaw so ChangeOper can's call ChangeOperUnchecked.
| { | ||
| VarSetOps::OldStyleClearD(this, optCopyPropKillSet); | ||
|
|
||
| bool updateFlags = false; |
|
|
||
| return gtNewHelperCallNode(helper, type, callFlags, argList); | ||
| GenTreeCall* result = gtNewHelperCallNode(helper, type, argList); | ||
| result->gtFlags |= callFlags; |
There was a problem hiding this comment.
I believe that this, and the setting of callFlags above are now redundant, as this is done in gtNewHelperCallNode(), right?
There was a problem hiding this comment.
This line is not redundant since in this method callFlags can also have GTF_CALL_HOISTABLE. The setting of GTF_EXCEPT above is redundant and I will remove it.
| // addr - Address to check | ||
| // | ||
| // Return Value: | ||
| // True is address could be null; false otherwise |
There was a problem hiding this comment.
Perhaps should be "True if address ..."?
| // gtUpdateSideEffects: Update the side effects for statement tree nodes. | ||
| // | ||
| // Arguments: | ||
| // stmt - The statement to update side effects on |
There was a problem hiding this comment.
You need a description of the tree argument. Also, since tree is unused in the then clause below, and stmt is unused in the else clause, would it make sense to have two different versions of this method, especially since their behavior is quite different - in one case an entire statement is updated, and in another case, only the direct children of tree, and its ancestors.
| // any struct arguments. | ||
| // i.e. assert(((call->gtFlags & GTF_EXCEPT) != 0) || ((args->Current()->gtFlags & GTF_EXCEPT) == 0) | ||
| flagsSummary |= (args->Current()->gtFlags & GTF_EXCEPT); | ||
|
|
| else | ||
| { | ||
| src->gtFlags |= GTF_IND_NONFAULTING; | ||
| } |
There was a problem hiding this comment.
Not a big deal, but the above pattern appears a lot - it might be nice to extract it into a method.
|
|
||
| GenTreePtr gtNewIndexRef(var_types typ, GenTreePtr arrayOp, GenTreePtr indexOp); | ||
|
|
||
| GenTreeArrLen* gtNewArrLen(var_types typ, GenTreePtr arrayOp, int lenOffset); |
There was a problem hiding this comment.
Can we stick to using GenTree* in new code?
|
|
||
| inline GenTreeCall* Compiler::gtNewHelperCallNode(unsigned helper, var_types type, GenTreeArgList* args) | ||
| { | ||
| unsigned flags = this->s_helperCallProperties.NoThrow((CorInfoHelpFunc)helper) ? 0 : GTF_EXCEPT; |
There was a problem hiding this comment.
What's this-> for? Looks strange considering that you're accessing a static member.
| // curSsaName - The map from lclNum to its recently live definitions as a stack | ||
| // | ||
| // Return Value: | ||
| // true if copy propagation was performed for the tree; false otherwise. |
There was a problem hiding this comment.
It looks like the function doesn't return anything.
| } | ||
| #endif | ||
| break; | ||
| return; |
There was a problem hiding this comment.
This change looks odd, considering that the function ends right after the loop and there's even a useless return after the loop.
|
|
||
| case GT_CALL: | ||
|
|
||
| GenTreeCall* call; |
There was a problem hiding this comment.
This variable seems unnecessary, it's used only once.
| } | ||
| } | ||
|
|
||
| genTreeOps oper = tree->OperGet(); |
|
|
||
| if (tree->OperMayThrow(this)) | ||
| { | ||
| tree->gtFlags |= GTF_EXCEPT; |
There was a problem hiding this comment.
Maybe we should add some flags setters to GenTree, this stuff keeps repeating.
tree->SetFlag(GTF_EXCEPT, tree->OperMayThrow(this)) for example.
| if (argx->gtOper == GT_OBJ) | ||
| { | ||
| argx->gtFlags &= ~(GTF_ALL_EFFECT) | (argx->AsBlk()->Addr()->gtFlags & GTF_ALL_EFFECT); | ||
| if (argx->OperMayThrow(this)) |
There was a problem hiding this comment.
I'm not usually a fan of ?: but sometimes it seems useful: argx->gtFlags |= argx->OperMayThrow(this) ? GTF_EXCEPT : GTF_IND_NONFAULTING;
| dest->gtFlags |= (GTF_EXCEPT | GTF_GLOB_REF | GTF_IND_TGTANYWHERE); | ||
| tree->gtFlags |= (GTF_EXCEPT | GTF_GLOB_REF | GTF_IND_TGTANYWHERE); | ||
| dest->gtFlags |= (GTF_GLOB_REF | GTF_IND_TGTANYWHERE); | ||
| tree->gtFlags |= (GTF_GLOB_REF | GTF_IND_TGTANYWHERE); |
There was a problem hiding this comment.
Is it correct to set GTF_IND_TGTANYWHERE on tree? Seems like it is a GT_ASG, not a GT_IND.
There was a problem hiding this comment.
It's pre-existing but I don't think it's correct. I'll try to remove it.
| case CORINFO_HELP_MON_ENTER: | ||
| case CORINFO_HELP_MON_EXIT: | ||
| case CORINFO_HELP_MON_ENTER_STATIC: | ||
| case CORINFO_HELP_MON_EXIT_STATIC: |
There was a problem hiding this comment.
Are the monitor helpers really "no throw"? Seems like "exit" could throw SynchronizationLockException.
| // New GT_IND node | ||
|
|
||
| inline GenTreePtr Compiler::gtNewIndir(var_types typ, GenTreePtr addr) | ||
| { |
|
@CarolEidt @mikedn @briansull Thank you for the review. I pushed a commit that addresses your comments. PTAL. |
|
@dotnet-bot test Windows_NT x64 Release Priority 1 Build and Test |
|
@dotnet-bot test Windows_NT jitstress1 |
|
@dotnet-bot test Windows_NT arm Cross Checked Build and Test |
JosephTremoulet
left a comment
There was a problem hiding this comment.
Thanks for cleaning this up! Looks good w/ a few comments.
| assert(!OperIsConst(oper)); // use ChangeOperLeaf() instead | ||
|
|
||
| unsigned mask = GTF_COMMON_MASK; | ||
| if (this->OperIsIndirOrArrLength() && OperIsIndir(oper)) |
There was a problem hiding this comment.
Why is this assymetric? I presume we never ChangeOper from vanilla indir to array length, but if we did, wouldn't we want to propagate the flag?
| inline void GenTree::ChangeOperUnchecked(genTreeOps oper) | ||
| { | ||
| unsigned mask = GTF_COMMON_MASK; | ||
| if (this->OperIsIndirOrArrLength() && OperIsIndir(oper)) |
| // contribution to its argument). | ||
| #define GTF_IND_VOLATILE 0x40000000 // GT_IND -- the load or store must use volatile sematics (this is a nop on X86) | ||
| #define GTF_IND_NONFAULTING 0x20000000 // GT_IND -- An indir that cannot fault. | ||
| #define GTF_IND_NONFAULTING 0x20000000 // Operations for which OperIsIndirOrArrLength() is true -- An indir that cannot fault. |
There was a problem hiding this comment.
I'd recommend defining GTF_ARRLEN_NONFAULTING where the other GTF_ARRLEN flags are defined and adding a static_assert_no_msg that the two flags agree -- that'll make it easier not to mistakenly try to re-use that bit if somebody goes to add another flag for array length nodes in the future. We currently do the same for GTF_FLD_VOLATILE and GTF_CLS_VAR_VOLATILE (and the static assertion is over by their use in morph that requires them to be the same).
|
@JosephTremoulet Good points, thanks. I pushed a commit that addresses your comments. |
|
|
||
| #define GTF_ARR_BOUND_INBND 0x80000000 // GT_ARR_BOUNDS_CHECK -- have proved this check is always in-bounds | ||
|
|
||
| #define GTF_ARRLEN_NONFAULTING 0x20000000 // GT_ARR_LENGTH -- An array length operation that cannot fault. Same as GT_IND_NONFAULTING. |
There was a problem hiding this comment.
Mind moving this down a line to be below GTF_ARRLEN_ARR_IDX? The flags in this file are typically listed in descending numerical order within each group.
pgavlin
left a comment
There was a problem hiding this comment.
This generally looks good, just a couple questions.
It does look like this primarily targets the frontend: do you plan on adding side-effect checks to LIR::Range::CheckLIR() as a follow-up change?
| break; | ||
|
|
||
| case GT_MEMORYBARRIER: | ||
| chkFlags |= GTF_GLOB_REF | GTF_ASG; |
There was a problem hiding this comment.
I'm a bit surprised that GT_MEMORYBARRIER does not imply GT_ORDER_SIDEEFF...
There was a problem hiding this comment.
I'm surprised by that too but the meaning of GTF_ORDER_SIDEEFF and GTF_GLOB_REF is not very well defined at the moment so I deferred the work to fix and check them and just kept the existing behavior.
There was a problem hiding this comment.
GTF_ORDER_SIDEEFF is well defined: it is a full compiler barrier. I suppose that when you say that its meaning is not well defined you're referring to the fact that we don't always check it?
There was a problem hiding this comment.
I don't think it's currently always used that way. In any case, checking GTF_ORDER_SIDEFF is a future work item and I'll investigate why it's not set on GT_MEMORYBARRIER at that time.
|
|
||
| CorInfoHelpFunc helper; | ||
| helper = comp->eeGetHelperNum(this->AsCall()->gtCallMethHnd); | ||
| if ((helper == CORINFO_HELP_UNDEF) || !comp->s_helperCallProperties.NoThrow(helper)) |
There was a problem hiding this comment.
Why not just
return (helper == CORINFO_HELP_UNDEF) || !comp->s_helperCallProperties.NoThrow(helper);
?
There was a problem hiding this comment.
No reason. I pushed a commit with an update.
|
I'll include adding side-effect checks to LIR::Range::CheckLIR() to my list of follow-up changes. |
Cool. There is a fundamental difference between side-effects in HIR and LIR in that the flags on a node do not need to be a summary of that node's side effects along with those of its operands: instead, the flags for a node should reflect only that node's side effects. |
|
Rerunning the job. Link to the failure: https://ci.dot.net/job/dotnet_coreclr/job/master/job/checked_ubuntu_tst_prtest/9021/ |
|
@dotnet-bot test Ubuntu x64 Checked Build and Test |
|
no, I was not aware this test was failing. Is this run with other tests? 'cause it's not supposed to. I am not sure if some test config has changed... @swgillespie do you know anything about this? |
|
@dotnet-bot test Ubuntu x64 Checked Build and Test |
1 similar comment
|
@dotnet-bot test Ubuntu x64 Checked Build and Test |
|
@maonis @swgillespie I have opened #13736 on the GC.API.NoGCRegion.NoGC.NoGC test failure |
|
@jkotas thanks! |
fgDebugCheckFlags is modified to check that GTF_EXCEPT and GTF_ASG are set precisely when needed.
It's also modified to handle several special operators correctly.
fgAddrCouldBeNull is updated to check for handles, implicit byref locals, and stack byrefs.
OperMayThrow is modified to handle several operators correctly.
GTF_IND_NONFAULTING is reused on operations for which OperIsIndir() is true and on GT_ARR_LENGTH.
Various places in morph are updated to set side effect flags correctly.
gtUpdateSideEffects is re-written so that it's precise for GTF_ASG and GTF_EXCEPT
and conservatively correct for the other side effects. It's now called from more places
to keep the flags up-to-date after transformations.
NoThrow in HelperCallProperties is updated and GTF_EXCEPT flag is set on helper calls according to
that property.
optRemoveRangeCheck is cleaned up and simplified.