Uh oh!
There was an error while loading. Please reload this page.
Optimization for full range checks (#70145) - #70222
Conversation
ghost
commented
Jun 3, 2022
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch Issue DetailsI also added code for comparison with int.MinValue/long.MinValue in case of int/long.
|
SkiFoD
commented
Jun 7, 2022
@jakobbotsch Hey, could you please become my reviewer? |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
danmoseley
commented
Jun 11, 2022
| { | ||
| GenTree* ret = gtNewZeroConNode(TYP_INT); | ||
| if (gtTreeHasSideEffects(cmp, GTF_SIDE_EFFECT)) | ||
| { | ||
| return cmp; | ||
| } |
There was a problem hiding this comment.
Trying to presave the side effects gave me many regressions, so I decided to cut it out for now.
| if ((op == GT_LT && lefOpValue == rightOpValue) || (op == GT_LE && lefOpValue > rightOpValue)) | ||
| { | ||
| GenTree* ret = gtNewZeroConNode(TYP_INT); | ||
| if (gtTreeHasSideEffects(cmp, GTF_SIDE_EFFECT)) | ||
| { | ||
| return cmp; | ||
| } | ||
| ret->SetVNsFromNode(cmp); | ||
| DEBUG_DESTROY_NODE(cmp); | ||
| INDEBUG(ret->gtDebugFlags |= GTF_DEBUG_NODE_MORPHED); | ||
| return ret; | ||
| } |
There was a problem hiding this comment.
It doesn't seem right that this can only fold things into false. What about when the comparison is always true?
There was a problem hiding this comment.
I can't think of a case when the comparison is always true. There are 7 typical trees which I'm testing on (value types may vary but the trees are always generalized to the 7 cases):
- When const is on the right and MinValue
examplev >= int.MinValue:
* RETURN int
\--* EQ int
+--* LT int
| +--* LCL_VAR int V00 arg0
| \--* CNS_INT int -0x80000000
\--* CNS_INT int 0
- When const is on the left and MinValue
exampleint.MinValue <= v
* RETURN int
\--* EQ int
+--* GT int
| +--* CNS_INT int -0x80000000
| \--* LCL_VAR int V00 arg0
\--* CNS_INT int 0
- When const is on the right and MaxValue
examplev <= int.MaxValue
* RETURN int
\--* EQ int
+--* GT int
| +--* LCL_VAR int V00 arg0
| \--* CNS_INT int 0x7FFFFFFF
\--* CNS_INT int 0
- When const is on the left and MaxValue
exampleint.MaxValue >= v
* RETURN int
\--* EQ int
+--* LT int
| +--* CNS_INT int 0x7FFFFFFF
| \--* LCL_VAR int V00 arg0
\--* CNS_INT int 0
- When const is ulong/uint and value is MinValue
examplev >= ulong.MinValue
exampleulong.MinValue <= v
* RETURN int
\--* CNS_INT int 1
- When const is on the right and value type is ulong/uint and value is MaxValue
examplev <= ulong.MaxValue;
\--* EQ int
+--* GT int
| +--* LCL_VAR long V00 arg0
| \--* CNS_INT long -1
\--* CNS_INT int 0
- When const is on the left and value type is ulong/uint and value is MaxValue
exampleulong.MaxValue >= v
* RETURN int
\--* EQ int
+--* LT int
| +--* CNS_INT long -1
| \--* LCL_VAR long V00 arg0
\--* CNS_INT int 0
There was a problem hiding this comment.
So for example:v >= int.MinValue generates a tree that is equal to return (v < int.MinValue) == falseint.MinValue <= v generates a tree that is equal to return (int.MinValue > v) == false
However in case of ulong/uint:v <= ulong.MaxValue generates a tree that is equal to return (v > -1) == false ulong.MaxValue >= vgenerates a tree that is equal toreturn (-1 < v) == false
These trees look odd to me because (v > -1) is going to be always true.
Correct me please if I'm wrong.
There was a problem hiding this comment.
One example would be:bool Foo(sbyte i) => i > -129;
There was a problem hiding this comment.
We generally do not see x >= y much here because there is no such IL instruction, so that's why the more "common" pattern i >= sbyte.MinValue is reversed by Roslyn. But we can still see such IR if we introduce it ourselves, so I think it makes sense to handle it (and also the above case).
There was a problem hiding this comment.
Do you know how to generate the always true condition but with GT_LE? I'm considering should we be bothered with such a case at all.
There was a problem hiding this comment.
Given that you already have the intervals I think the actual check itself is so simple that there is no reason not to add it, it should not be more than a couple of lines.
[x0, x1] <= [y0, y1] is always true if x1 <= y0.
There was a problem hiding this comment.
I tried to apply this condition, which considering only LT for simplicity and it gave me 1700+ improvements during the spmi asmdiff run, which is suspicious :)
if (((op == GT_LT) && (lhsMin >= rhsMax)) || (((op == GT_LE) && (lhsMin > rhsMax))))
{
ret = gtNewZeroConNode(TYP_INT);
}
else if ((op == GT_LT) && rhsMax > lhsMax)
{
ret = gtNewOneConNode(TYP_INT);
}
Then I tried to check which Op is const to make the condition more strict:
//When lhs is constant
* RETURN int
\--* LT int
+--* CNS_INT int -129
\--* LCL_VAR byte V00 arg0
else if ((op == GT_LT) && rhsMin > lhsMin && lhsMin == lhsMax)
{
ret = gtNewOneConNode(TYP_INT);
}
// When rhs is constant
For cases like this:
* RETURN int
\--* LT int
+--* LCL_VAR byte V00 arg0
\--* CNS_INT int 129
else if ((op == GT_LT) && rhsMax > lhsMax && rhsMin == rhsMax)
{
ret = gtNewOneConNode(TYP_INT);
}
It works fine and there are not so many improvements. What do you think of this?
There was a problem hiding this comment.
[x0, x1] < [y0, y1] is true if x1 < y0. So I thinkelse if ((op == GT_LT) && rhsMax > lhsMax)
should beelse if ((op == GT_LT) && lhsMax < rhsMin).
| int64_t lefOpValue = lhsMin; | ||
| int64_t rightOpValue = rhsMax; | ||
| if (cmp->IsUnsigned() && lefOpValue == -1 && lhsMax == -1) | ||
| { | ||
| rightOpValue = -1; | ||
| } |
There was a problem hiding this comment.
I don't like the part of code, but I haven't come up with an idea how to get ride of the magic number (-1) yet. The idea here is that ulong and uint use this const.
For example: bool Test_M27(ulong v) => v <= ulong.MaxValue generates this tree:
* RETURN int
\--* EQ int
+--* GT int
| +--* LCL_VAR long V00 arg0
| \--* CNS_INT long -1
\--* CNS_INT int 0
There was a problem hiding this comment.
For unsigned comparisons a signed interval that spans 0 represents two distinct intervals, i.e. for [x, y]:
- if
y < 0: represents[(unsigned)x, (unsigned)y] - if
x >= 0: represents[x, y] - else: represents
[0, y] U [(unsigned)x, MaxValue]
So the "generalized" code here would need to check these conditions. I agree this is a bit tricky, so it is fine with me to special case this, although it would be nice to find a way to shape the code such that we avoid the "normal" check below in the special case.
Also, I think the lefOpValue and rightOpValue are a bit confusing, I would just try to stick with lhsMin/Max and rhsMin/Max.
There was a problem hiding this comment.
The tricky part here is that when rhs is ulong thenrhsMin = IntegralRange::SymbolicToRealValue(rhsRange.GetLowerBound()); returns -9223372036854775808 instead of 0.
So what if we would use something like this:
else if (cmp->IsUnsigned() && (op == GT_LT) && rhsMin < 0 && !isLhsConst)
{
ret = gtNewZeroConNode(TYP_INT);
}
else if (cmp->IsUnsigned() && (op == GT_LT) && lhsMin < 0 && isLhsConst)
{
ret = gtNewZeroConNode(TYP_INT);
}
There was a problem hiding this comment.
For unsigned comparisons a signed interval that spans 0 represents two distinct intervals, i.e. for
[x, y]:
- if
y < 0: represents[(unsigned)x, (unsigned)y]- if
x >= 0: represents[x, y]- else: represents
[0, y] U [(unsigned)x, MaxValue]So the "generalized" code here would need to check these conditions. I agree this is a bit tricky, so it is fine with me to special case this, although it would be nice to find a way to shape the code such that we avoid the "normal" check below in the special case.
Could you please explain this idea in more details, I'm not sure I can understand how to apply this.
There was a problem hiding this comment.
I guess you would do something like:
if (cmp->IsUnsigned())
{
if ((lhsMin < 0) && (lhsMax >= 0))
{
// [0, (uint64_t)lhsMax] U [(uint64_t)lhsMin, MaxValue]
lhsMin = 0;
lhsMax = -1;
}
if ((rhsMin < 0) && (rhsMax >= 0))
{
// [0, (uint64_t)rhsMax] U [(uint64_t)rhsMin, MaxValue]
rhsMin = 0;
rhsMax = -1;
}
}
int foldValue;
if (cmp->IsUnsigned())
{
if ((op == GT_LT && ((uint64_t)lhsMax < (uint64_t)rhsMin) ||
(op == GT_LE && ((uint64_t)lhsMax <= (uint64_t)rhsMin))
{
foldValue = 1;
}
elseif ((op == GT_LT && ((uint64_t)lhsMin >= (uint64_t)rhsMax) ||
(op == GT_LE && ((uint64_t)lhsMin > (uint64_t)rhsMax))
{
foldValue = 0;
}
else
{
return;
}
}
else
{
if ((op == GT_LT && (lhsMax < rhsMin) ||
(op == GT_LE && (lhsMax <= rhsMin))
{
foldValue = 1;
}
elseif ((op == GT_LT && (lhsMin >= rhsMax) ||
(op == GT_LE && (lhsMin > rhsMax))
{
foldValue = 0;
}
else
{
return;
}
}
// fold to foldValue hereBut, it's hard to get all the cases right :-) I would probably double check with something like https://github.com/jakobbotsch/Fuzzlyn. I will definitely run that on this PR before merging.
There was a problem hiding this comment.
For example, running Fuzzlyn on your PR in the current shape quickly finds examples. I did:
> .\Fuzzlyn.exe--host C:\dev\dotnet\runtime2\artifacts\tests\coreclr\windows.x64.Checked\Tests\Core_Root\corerun.exe--num-programs 10000000--parallelism 8
Found example with seed 10360326530109389226> .\Fuzzlyn.exe--host C:\dev\dotnet\runtime2\artifacts\tests\coreclr\windows.x64.Checked\Tests\Core_Root\corerun.exe--reduce --seed 10360326530109389226
Simplifying Coarsely. Total elapsed: 00:00:10. Method 51/51.
Simplifying Statements. Total elapsed: 00:00:15. Iter: 107/107
Simplifying Expressions. Total elapsed: 00:00:16. Iter: 478/478
Simplifying Members. Total elapsed: 00:00:19. Iter: 9/9
Simplifying Statements. Total elapsed: 00:00:19. Iter: 12/12
Simplifying Expressions. Total elapsed: 00:00:19. Iter: 45/45
Simplifying Members. Total elapsed: 00:00:20. Iter: 7/7
Simplifying Statements. Total elapsed: 00:00:20. Iter: 8/8
Simplifying Expressions. Total elapsed: 00:00:20. Iter: 35/35
Simplifying Members. Total elapsed: 00:00:20. Iter: 6/6
Simplifying Statements. Total elapsed: 00:00:20. Iter: 8/8
Simplifying Expressions. Total elapsed: 00:00:20. Iter: 34/34
Simplifying Members. Total elapsed: 00:00:20. Iter: 6/6which outputs:
// Generated by Fuzzlyn v1.5 on 2022-06-14 19:20:16// Run on X64 Windows// Seed: 10360326530109389226// Reduced from 64.1 KiB to 0.5 KiB in 00:00:23// Debug:// Release: Outputs 0publicclassProgram{publicstaticIRuntimes_rt;publicstaticuints_4;publicstaticvoidMain(){s_rt=newRuntime();boolvr1=M1(0);}publicstaticboolM1(shortarg0){if((12729537629719743250UL<(uint)arg0)){s_rt.WriteLine(s_4);}returntrue;}}publicinterfaceIRuntime{voidWriteLine<T>(Tvalue);}publicclassRuntime:IRuntime{publicvoidWriteLine<T>(Tvalue)=>System.Console.WriteLine(value);}This program is incorrect with the current PR.
There was a problem hiding this comment.
Looks like an amazing tool. I played with it a little bit and got something like this:
// Generated by Fuzzlyn v1.5 on 2022-06-15 09:15:55// Run on X64 Windows// Seed: 6538447736931409473// Reduced from 109.8 KiB to 0.2 KiB in 00:00:53// Debug: Outputs True// Release: Outputs FalsepublicclassProgram{publicstaticlongs_33=1;publicstaticbools_50;publicstaticvoidMain(){uintvr0=(uint)(-s_33);s_50=4038847739U<vr0;System.Console.WriteLine(s_50);}}Does it mean that if I cut out the code (from main) and build it in Debug then it returns True and if I build it in Release then it returns False?
There was a problem hiding this comment.
Yes, that's what it means (and also tiered compilation has to be disabled).
There was a problem hiding this comment.
FWIW, your treatment of unsigned comparisons is still wrong, you cannot use the signed comparisons for the interval checks in that case. It is probably the reason for this problem. I would suggest you shape the code somewhat like the example I posted earlier.
| rightOpValue = -1; | ||
| } | ||
| if ((op == GT_LT && lefOpValue == rightOpValue) || (op == GT_LE && lefOpValue > rightOpValue)) |
There was a problem hiding this comment.
Shouldn't the first check be generalized to lhsMin >= rhsMax?
Also, I think this still needs special handling for unsigned comparisons. The easiest is probably to bail for negative intervals except for your special case above. For positive intervals you can use the signed comparisons.
There was a problem hiding this comment.
Shouldn't the first check be generalized to
lhsMin >= rhsMax?
It may work with GT_LT, but what about GT_LE?
If it is GT_LE then lhsMin>rhsMax and lhsMin>=rhsMax have different results. Right?
There was a problem hiding this comment.
Yes, by "first check" I meant the GT_LT part.
There was a problem hiding this comment.
Then it would look like if ((op == GT_LE && lefOpValue > rightOpValue) || lhsMin>=rhsMax)
There was a problem hiding this comment.
Ah, I just meant((op == GT_LT) && (lhsMin >= rhsMax)) || ((op == GT_LE) && (lhsMin > rhsMax)))
Cut out the unsigned types to make sure the rest works fine
| // 1. The unmodified "cmp" tree. | ||
| // 2. A CNS_INT node containing zero. | ||
| // 3. A GT_COMMA node containing side effects along with a CNS_INT node containing zero |
There was a problem hiding this comment.
Ditto for the "Always false" in the summary above.
| if (ret != nullptr) | ||
| { | ||
| ret->SetVNsFromNode(cmp); |
There was a problem hiding this comment.
| ret->SetVNsFromNode(cmp); | |
| fgUpdateConstTreeValueNumber(ret); |
(Given that we folded, this is more precise)
commented
Jun 16, 2022
/azp run Fuzzlyn |
commented
Jun 16, 2022
|
Azure Pipelines successfully started running 1 pipeline(s). |
commented
Jun 17, 2022
@jakobbotsch I was testing the code via Fuzzlyn and found 1 seed out of 10000, then I tried to debug the reduced piece of code, but It threw an exception (assertion was false) even though it never went through the new lines of code written by me, which was suspicious. I repeated the same experiment but on the main branch, without any line of my code, and the result was the same. Can Fuzzlyn generate programs that work with errors on current runtime-HEAD? If yes, then is there a rule of thumb how to make sure Fuzzlyn says my code doesn't break anything? |
left a comment
There was a problem hiding this comment.
Looks great to me now, thanks! The Fuzzlyn run has only known failures.
We currently have some issues with the superpmi-diffs pipeline that means we cannot evaluate throughput impact, but I am hoping this will be resolved soon.
commented
Jun 17, 2022
What was the assertion error? There are some known failures currently. We don't have a good way to signal what is a "known failure", but you can try searching the repo issues to see if an issue is already open for it. |
commented
Jun 17, 2022 •
I assume this is a known issue then :) |
commented
Jun 17, 2022
I does look great now. Thank you for your guidence. I would never be able to accomplish this without your help, although the issue was marked as easy 👍 |
commented
Jun 17, 2022
commented
Jun 17, 2022
Do you want me to merge the main-HEAD branch changes to this one? |
commented
Jun 17, 2022
No, that's not necessary -- the CI jobs already do such a merge before running. |
commented
Jun 17, 2022
Many nice diffs that look like the following: ; Assembly listing for method Microsoft.CodeAnalysis.EnumBounds:IsValid(ubyte):bool
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; optimized code
; rsp based frame
; partially interruptible
; No matching PGO data
; Final local variable assignments
;
; V00 arg0 [V00,T00] ( 3, 3 ) ubyte -> rcx single-def
;# V01 OutArgs [V01 ] ( 1, 1 ) lclBlk ( 0) [rsp+00H] "OutgoingArgSpace"
-; V02 cse0 [V02,T01] ( 3, 2.50) int -> rax "CSE - aggressive"
;
; Lcl frame size = 0
-G_M51999_IG01: ; gcVars=0000000000000000 {}, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, gcvars, byref, nogc <-- Prolog IG+G_M51999_IG01: ; gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref, nogc <-- Prolog IG
;; size=0 bbWeight=1 PerfScore 0.00
-G_M51999_IG02: ; gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref, isz- movzx rax, cl- test eax, eax- jl SHORT G_M51999_IG05- ;; size=7 bbWeight=1 PerfScore 1.50-G_M51999_IG03: ; gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref- cmp eax, 1- setle al- movzx rax, al- ;; size=9 bbWeight=0.50 PerfScore 0.75-G_M51999_IG04: ; , epilog, nogc, extend- ret- ;; size=1 bbWeight=0.50 PerfScore 0.50-G_M51999_IG05: ; gcVars=0000000000000000 {}, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, gcvars, byref+G_M51999_IG02: ; gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref
xor eax, eax
- ;; size=2 bbWeight=0.50 PerfScore 0.12-G_M51999_IG06: ; , epilog, nogc, extend+ cmp cl, 1+ setbe al+ ;; size=8 bbWeight=1 PerfScore 1.50+G_M51999_IG03: ; , epilog, nogc, extend
ret
- ;; size=1 bbWeight=0.50 PerfScore 0.50+ ;; size=1 bbWeight=1 PerfScore 1.00-; Total bytes of code 20, prolog size 0, PerfScore 5.38, instruction count 9, allocated bytes for code 20 (MethodHash=704934e0) for method Microsoft.CodeAnalysis.EnumBounds:IsValid(ubyte):bool+; Total bytes of code 9, prolog size 0, PerfScore 3.40, instruction count 4, allocated bytes for code 9 (MethodHash=704934e0) for method Microsoft.CodeAnalysis.EnumBounds:IsValid(ubyte):boolIn many cases we also remove entire basic blocks because they are now unreachable. |
commented
Jun 18, 2022
/azp run runtime-coreclr superpmi-diffs |
commented
Jun 18, 2022
|
Azure Pipelines successfully started running 1 pipeline(s). |
commented
Jun 19, 2022
As one would expect the throughput impact on x86 is a bit higher than 64-bit platforms, but I think it is at an acceptable level where keeping the code path uniform is preferable. windows x64
windows x86
|
fixes#70145
I also added code for comparison with int.MinValue/long.MinValue in case of int/long.