Full support for exception sets in value numbering.#20129
Full support for exception sets in value numbering.#20129briansull merged 2 commits intodotnet:masterfrom
Conversation
src/jit/valuenum.cpp
Outdated
| if (GetVNFunc0Map()->Lookup(func, &res)) | ||
| // Have we already assigned a ValueNum for 'func' ? | ||
| // | ||
| if (GetVNFunc0Map()->Lookup(func, &resultVN)) |
There was a problem hiding this comment.
Perhaps it could more easy to read if if condition would be negated and fused with else statement.
src/jit/valuenum.cpp
Outdated
| #endif | ||
|
|
||
| //---------------------------------------------------------------------------------------- | ||
| // VNForFunc-2 - Returns the ValueNum associated with 'func'('arg0VN','arg1VN') |
There was a problem hiding this comment.
From docs perspective VNForFunc-2 could be misleading since it could indicate function name. In reality func name is VNForFunc but is defined as different overload what cen be immediately seen from list of args
There was a problem hiding this comment.
I think that what I have done with the number suffix -1 -2 -3 is fine here.
As a function name can never contain a "-" symbol.
There was a problem hiding this comment.
@briansull
I am raising this problem bcs I have started working on automatic generation of docs for native code using doxygen. First attempts look promising. Current documentation format is not supported by doxygen and good quality docs generation would require applying transforms which most probably could be implemented either by using clang-doxygen build or by sophisticated regex. Having to recognize invalid function name and match it with signature in code could be an additional problem preventing docs generation.
There was a problem hiding this comment.
How does doxygen differenentiate between function overloads with the same name?
There was a problem hiding this comment.
doxygen uses either its own fully functional C++ parser or even better integrates with Clang and relies on Clang parser for C/C++ code understanding.
There was a problem hiding this comment.
Function overloads are displayed in a similar way as C# overloads are displayed in C# docs.
There was a problem hiding this comment.
That is the full prototype with all of the types for all arguments, right?
Write(String, Object, Object, Object, Object)
Write(String, Object, Object, Object)
| { | ||
| // We can fold the expression, but we don't want to fold | ||
| // when the expression will always throw an exception | ||
| shouldFold = VNEvalShouldFold(typ, func, arg0VN, arg1VN); |
There was a problem hiding this comment.
I have analyzed similar problem while writing folding check in importer and it seems that there are 2 possible scenarios with 2 possible decisions:
- Expression tree is much bigger in comparison to tree with exception -> decision: it is better to fold.
- Expression tree is comparable or smaller than exception tree -> decision: do not fold.
There was a problem hiding this comment.
Actually the reason that VNEvalShouldFold returns false is that the folding of some expressions would turn them into an unconditional throw. When the expression unconditionally throws it does not produce a useful "normal" value number and using a dummy value such a zero causes problems because we think that we can use that zero in the CSE phase.
It is simpler to detect such cases and not try to fold them in value numbering.
So we do not fold (5 / 0) or (MIN_INT / -1)
src/jit/valuenum.cpp
Outdated
| } | ||
| break; | ||
| //---------------------------------------------------------------------------------------- | ||
| // VNForFunc-3 - Returns the ValueNum associated with 'func'('arg0VN','arg1VN','arg2VN') |
There was a problem hiding this comment.
The same docs problem as above: function name has not changed.
There was a problem hiding this comment.
I think that what I have done with the number suffix -1 -2 -3 is fine here.
As a function name can never contain a "-" symbol.
| VNFunc fgValueNumberHelperMethVNFunc(CorInfoHelpFunc helpFunc); | ||
| // Requires that "helpFunc" is one of the pure Jit Helper methods. | ||
| // Returns the corresponding VNFunc to use for value numbering | ||
| VNFunc fgValueNumberJitHelperMethodVNFunc(CorInfoHelpFunc helpFunc); |
There was a problem hiding this comment.
Consistency with existing code can be a good thing but maybe it's time to stop using fg for everything. All these new functions should have a vn prefix.
There was a problem hiding this comment.
Yeah, I know, I can do that in an upcoming changeset
There was a problem hiding this comment.
OK I will change the names of these methods to start with vnAddExceptionSet
src/jit/valuenumfuncs.h
Outdated
| ValueNumFuncDef(ArithmeticExc, 2, false, false, false) // E.g., for signed its, MinInt / -1. | ||
| ValueNumFuncDef(OverflowExc, 1, false, false, false) // Integer overflow check. Args: 0: expression value, throws when it overflows | ||
| ValueNumFuncDef(ArithmeticExc, 2, false, false, false) // Arithmetic exception check, ckfinite and integer division overflow, Args: 0: expression value, | ||
| ValueNumFuncDef(OverflowExc, 1, false, false, false) // Integer overflow check. used for chjecked add,sub and mul Args: 0: expression value, throws when it overflows |
src/jit/valuenum.cpp
Outdated
| ValueNum resultVN = NoVN; | ||
|
|
||
| // We may create one of these in the switch below. | ||
| ValueNum ZeroVN; |
There was a problem hiding this comment.
These should be moved inside the switch
src/jit/valuenum.cpp
Outdated
| if (typ == TYP_BYREF) // We don't want/need to optimize a zero byref | ||
| { | ||
| return res; | ||
| return NoVN; |
| ZeroVN = VNZeroForType(typ); | ||
| if (arg1VN == ZeroVN) | ||
| { | ||
| resultVN = arg0VN; |
src/jit/valuenum.cpp
Outdated
| // This identity does not apply for floating point (when x == -0.0) | ||
| // (x + 0) == (0 + x) => x | ||
| ZeroVN = VNZeroForType(typ); | ||
| if (VNIsEqual(arg0VN, ZeroVN)) |
There was a problem hiding this comment.
Maybe it would be clearer to just wrap the whole thing in an if (!varTypeIsFloating(typ)) instead of relying on VNIsEqual.
There was a problem hiding this comment.
I will rework this area to be clearer
| // First we'll record the exeception set for the rhs and | ||
| // later we will union in the exeception set for the lhs | ||
| // | ||
| ValueNum vnExcSet = ValueNumStore::VNForEmptyExcSet(); |
There was a problem hiding this comment.
Wouldn't be better to change VNUnpackExc to always initialize pvnx rather than doing this all other the place?
src/jit/valuenum.cpp
Outdated
| if (vnStore->IsVNConstant(funcCAttr.m_args[1]) && | ||
| varTypeIsIntegral(vnStore->TypeOfVN(funcCAttr.m_args[1]))) | ||
| { | ||
| offset += vnStore->CoercedConstantValue<ssize_t>(funcCAttr.m_args[1]); |
There was a problem hiding this comment.
Hmm, I don't understand this. Looks like the total offset is extracted from both liberal and conservative VNs?
There was a problem hiding this comment.
I redid this area and fixed it. Thanks
src/jit/valuenum.cpp
Outdated
|
|
||
| if (typ == TYP_INT) | ||
| { | ||
| INT32 kVal; |
There was a problem hiding this comment.
Move inside ifs below. There are more cases below.
src/jit/valuenum.cpp
Outdated
| ValueNumPair vnpTreeExc = ValueNumStore::VNPForEmptyExcSet(); | ||
| ValueNumPair vnpDivZeroExc = ValueNumStore::VNPForEmptyExcSet(); | ||
| ValueNumPair vnpArithmExc = ValueNumStore::VNPForEmptyExcSet(); | ||
| ValueNumPair newExcSet; |
There was a problem hiding this comment.
Move it below, where it is initialized.
src/jit/valuenum.cpp
Outdated
| // | ||
| ValueNumPair vnpTreeNorm; | ||
| ValueNumPair vnpTreeExc = ValueNumStore::VNPForEmptyExcSet(); | ||
| ValueNumPair newExcSet; |
| // | ||
| ValueNumPair vnpTreeNorm; | ||
| ValueNumPair vnpTreeExc = ValueNumStore::VNPForEmptyExcSet(); | ||
| ValueNumPair newExcSet; |
| case GT_ARR_LENGTH: // Implicit null check. | ||
| case GT_IND: // Implicit null check. | ||
| case GT_NULLCHECK: // Explicit null check. | ||
| fgValueNumberAddExceptionSetForIndirection(tree); |
There was a problem hiding this comment.
About other indir-like nodes (atomic ops, HW intrinsics)?
Also, there's a GTF_IND_NONFAULTING flags, should that be tested here?
There was a problem hiding this comment.
If those nodes return true for OperMayThrow then they must be added here. (otherwise we assert in Debug)
If they don't, then it is a nop to add code to handle them here.
There was a problem hiding this comment.
OperMayThrow checks for the GTF_IND_NONFAULTING flag, so we don't need to handle it here.
There was a problem hiding this comment.
If those nodes return true for OperMayThrow then they must be added here
GT_HWIntrinsic can return true if it's a load/store. I don't see atomic ops in OperMayThrow but they probably should be there too.
|
@dotnet/jit-contrib PTAL I have investigated the diffs and they are very few and are a very small. |
|
Fixes #8648 and #10111 |
5a8d9f3 to
1fc0560
Compare
| { | ||
| unsigned int cseBit = genCSEnum2bit(tree->gtCSEnum); | ||
| CSEdsc* desc = optCSEfindDsc(tree->gtCSEnum); | ||
| unsigned CSEnum = GET_CSE_INDEX(tree->gtCSEnum); |
There was a problem hiding this comment.
Seems like somewhere in here (or perhaps in the method header) you should outline the general approach: the intention is to determine for each candidate if the intersection of all the exceptions provided by the defs is a superset of the union of the exceptions expected by the (qualified) uses.
The algorithm uncovers the defs and uses gradually and so incrementally building up both the intersection def set and the union use set.
It would be good to make sure we have (or can point at) test cases that show the various cases of this code getting covered.
I am still thinking about it but wonder if there is some implicit priority in choosing uses that should be thought about more carefully. Suppose we first find a def with exceptions 1, 2, 3 and then a use with exceptions 1, 2, 3, and then a def with exceptions 2, 3 and then a use with exceptions 2, 3. By the current algorithm we would not do any CSE here. But we could still CSE the 2,3 use.
So I wonder if instead we should not disqualify any uses until we've seen them all, and then pick the subset of uses that is compatible with what the definitions can provide.
There was a problem hiding this comment.
Typically expressions with the same Normal ValueNum generate exactly the same exception sets. There are two way that we can get different exception sets with the same Normal value number.
-
We used an arithmetic identiity:
e.g.:(p.a + q.b) * 0-- The normal value for the expression is set to zero because of the multiply by zero.
e.g.(p.a - p.a )-- The normal value for the expression is set to zero because of the subtraction of the same value. -
We stored an expression into a LclVar or into Memory and used it later in a CSE:
e.g.t = p.a; e1=(t + q.b); e2=(p.a+q.b)-- e1 has one NullPtrExc and e2 has two.
e.g.m.a = p.a; e1=(m.a + q.b); e2=(p.a + q.b)-- e1 and e2 have different exception sets.
The bug cases were due to case 1.
The small code regressions are due to case 2.
I believe that case 2 can be fixed by adding exception sets to the ValueNum that we use to track the Global Heap. After each operation we would update the Global Heap with any new NullPtrExc or other exception sets. Then the CSE uses could also rely upon exception sets tracked by the Global Heap.
There was a problem hiding this comment.
Added large comment addressing this to the method header
There was a problem hiding this comment.
Thanks for the comment -- my main concern was not how this can happen (though it is nice to now have specifics here), but how often it can happen.
As long as it's "rare" that different CSE defs have different exception sets then it doesn't matter much whether you disqualify uses eagerly or wait until you've seen them all, as it is unlikely you'll ever hit the case I describe where you miss CSEing some use because you saw some other more constraining use first.
But maybe it's worth mentioning that the approach you take here may not find the "maximal" set of CSEs in some rare cases.
| if (theConservNormVN != desc->defConservNormVN) | ||
| { | ||
| // This candidate has defs with differing conservative normal VNs, mark it with NoVN | ||
| desc->defConservNormVN = ValueNumStore::NoVN; // record the marker for differing VNs |
There was a problem hiding this comment.
This could also set NO_CSE and then continue, right?
There was a problem hiding this comment.
No, This is existing code that is there for rangecheck elimation. (I believe)
I renamed the existing field to `defConservNormVN' and maintained its existing behavior.
Here is the old existing field definition in compiler.h
ValueNum defConservativeVN; // if all def occurrences share the same conservative value
There was a problem hiding this comment.
Might be useful to mention how this can happen and why even if so, the candidate is still a viable CSE.
| // | ||
| if (desc->defExcSetCurrent != | ||
| theLiberalExcSet) // no update is needed when these are the same VN | ||
| { |
There was a problem hiding this comment.
I think the code reads better if you don't use these end of line comments.
Also it would be nice to try and bubble all the fail/continue cases upwards so that we don't get so deeply nested, eg structure the code as the loop-equivalent of early return style, eg:
if (something disqualifying 1)
{
continue;
}
if (something disqualifying 2)
{
continue;
}
// success !There was a problem hiding this comment.
Not sure exactly you want here. As far as I can tell this isn't a straight forward change.
There was a problem hiding this comment.
For now maybe just move the comments.
I think there may be simpler/cleaner ways to express the code too, but I'm ok leaving it as is for now.
AndyAyersMS
left a comment
There was a problem hiding this comment.
Haven't looked at the valuenum.cpp changes yet in much detail.
| // use to fetch the same value with no reload, so we can safely propagate that | ||
| // conservative VN to this use. This can help range check elimination later on. | ||
| cse->gtVNPair.SetConservative(defConservativeVN); | ||
| cse->gtVNPair.SetConservative(theConservativeVN); |
There was a problem hiding this comment.
This is the existing code that depends upon theConservativeVN getting set to NoVN when there are differing conservative values.
1fc0560 to
3e5b2a9
Compare
New method that add exception sets: fgValueNumberAddExceptionSet - fgValueNumberAddExceptionSetForIndirection - fgValueNumberAddExceptionSetForDivision - fgValueNumberAddExceptionSetForOverflow - fgValueNumberAddExceptionSetForCkFinite Refactoring work added methods: VNEvalShouldFold - method to decide if constant folding should be performed EvalUsingMathIdentity - Uses math identities to simplify value number exoressions Renamed fgValueNumberHelperMethVNFunc to fgValueNumberJitHelperMethodVNFunc Removed the suffixes from the method headers comments
3e5b2a9 to
a552cfe
Compare
|
@AndyAyersMS PTAL |
AndyAyersMS
left a comment
There was a problem hiding this comment.
Looked at valuenum.cpp now too.
|
@dotnet-bot retest Windows_NT x64 Release CoreFX Tests |
|
Fixes #8648 |
New method that add exception sets:
fgValueNumberAddExceptionSet
Refactoring work added methods:
VNEvalShouldFold - method to decide if constant folding should be performed
EvalUsingMathIdentity - Uses math identities to simplify value number exoressions
Renamed fgValueNumberHelperMethVNFunc to fgValueNumberJitHelperMethodVNFunc
Added standard method header comments for all five overloads of VNForFunc