-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Conversation
src/jit/morph.cpp
Outdated
// sign-extend incorrectly in cases where the NEG node ends up | ||
// feeding directly into a cast, for example (byte)(0 - s_1) | ||
// where s_1 is a byte. | ||
tree->gtOp.gtOp2 = op2 = gtNewOperNode(GT_NEG, tree->gtType, op2); |
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.
I noticed that this was already fixed for a different case (0 - s_1
). I'm not sure if there was a test for this already, but I have added it in my test case as well (and expanded on the 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.
The use of tree->gtType
is somewhat questionable. Since this basically transforms op2
into NEG(op2)
using the actual type of op2
would be more appropriate - gtNewOperNode(GT_NEG, genActualType(op2->TypeGet()), op2)
.
Using tree
's type implicitly assumes that it is the same type as the tree operand(s) type. That assumption holds today for binary opers like MUL
and SUB
but perhaps that won't always be the case (e.g. it might be useful to get rid of a flag like GTF_MUL_64RSLT
and instead explicitly represent a 32x32to64 bit multiplication in IR).
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.
Sure, I can make that change.
I'm curious: the ancestor tree (the parent of tree
) would in that future case have its child change type. Would more changes not be necessary to adapt to this? In the case I am fixing here, tree
ends up being completely replaced by its NEG
node (op2
), so using the type of tree
here means the type doesn't change.
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.
I'm curious: the ancestor tree (the parent of tree) would in that future case have its child change type. Would more changes not be necessary to adapt to this? In the case I am fixing here, tree ends up being completely replaced by its NEG node (op2), so using the type of tree here means the type doesn't change.
Sorry, I don't understand. Can you provide some sort of clarifying example?
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 this case the tree is morphed in separate steps like this:
ancestor
|
MUL <- `tree`
/ \
-1 ...
to
ancestor
|
MUL <- `tree`
/ \
1 NEG
|
...
to
ancestor
|
NEG
|
...
Of course this only happens because the multiplication is by -1
. So the NEG
node created here replaces tree
entirely. That (and the explicit comment) is why using the type of tree
seemed right to me.
In the future, if a MUL
node could take 64-bit operands, would this not need to further change anyway? Otherwise the ancestor's child changes type, which it perhaps wouldn't handle.
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.
So the NEG node created here replaces tree entirely
That is the result of a separate transform. If long MUL(int, int)
will ever be valid then any attempt to remove such a MUL
node will require checking the relevant types and inserting widening casts as needed. But transforms that introduces NEG
nodes won't ever be affected by such a change.
More generally - changing op
to unary(op)
with type = actualType(op)
should always be valid since it preserves the original type. Using op
's parent type works under the assumption that the parent type is the same as the operand type. That assumption is true for MUL
and many other binary opers but not for all - for example the type of a relop (EQ, NE etc.) oper can be different from the type of its operands.
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.
Right, makes sense. Thanks for the explanation.
src/jit/morph.cpp
Outdated
@@ -13183,7 +13191,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac) | |||
// if negative negate (min-int does not need negation) | |||
if (mult < 0 && mult != SSIZE_T_MIN) | |||
{ | |||
tree->gtOp.gtOp1 = op1 = gtNewOperNode(GT_NEG, op1->gtType, op1); | |||
tree->gtOp.gtOp1 = op1 = gtNewOperNode(GT_NEG, tree->gtType, op1); |
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.
I don't think this is technically necessary because this node always feeds into the GT_MUL
created below, which always assumes its operand is already widened. However from the explanations in #18238 I assume this is still preferable.
@dotnet-bot test Windows_NT x64 Checked CoreFX Tests please |
I'm not sure what the failing CI is. Looks like an infrastructure issue? |
Yeah, so it seems:
Unfortunately this happens from time to time. @dotnet-bot test OSX10.12 x64 Checked Innerloop Build and Test |
8490655
to
7a04ab3
Compare
When the JIT was morphing trees like '-1 * expr', it would turn the multiplication into a NEG node with the same type as its right operand. This is a problem when the right operand was a small type like TYP_UBYTE because the NEG node always produces a widened result. This could cause problems when the negation was fed into something that depended on the type, such as a cast to TYP_UBYTE: here the JIT would conclude that the cast could be dropped, and end up producing a widened result. The solution is to give the tree the actual type of the NEG node. Also add a test for this case and for a similar case of '0 - expr', which already had a fix. Fix #18780
7a04ab3
to
6051584
Compare
@dotnet-bot test OSX10.12 x64 Checked Innerloop Build and Test |
The test failure looks like #18858. Retrying again... |
@AndyAyersMS This is ready. Can you tag the appropriate people to review this? |
@erozenfeld PTAL |
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 a couple of comment and test suggestions.
Thanks for finding these bugs and fixing them!
src/jit/morph.cpp
Outdated
// Otherwise we may sign-extend incorrectly in cases where the NEG | ||
// node ends up feeding directly into a cast, for example (byte)(0 - s_1) | ||
// where s_1 is a byte. | ||
tree->gtOp.gtOp2 = op2 = gtNewOperNode(GT_NEG, genActualType(op2->TypeGet()), op2); |
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.
I think the comment is slightly confusing: you use byte
here in C# sense, where byte
is unsigned. TYP_BYTE
in the jit is signed. I think it would be more clear if you used the jit terminology, TYP_UBYTE
in this case.
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.
NEG node --> GT_NEG node
src/jit/morph.cpp
Outdated
// The type of the new GT_NEG node cannot just be op1->TypeGet(). | ||
// Otherwise we may sign-extend incorrectly in cases where the NEG | ||
// node ends up feeding directly a cast, for example (byte)(-1 * s_1) | ||
// where s_1 is a byte. |
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.
Same suggestions for this comment.
ok &= M1(0); | ||
ok &= M2(); | ||
ok &= M3(); | ||
return ok ? 100 : 0; |
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.
-1 is preferred for failure cases
@erozenfeld I have addressed your comments, thanks for the review! I wasn't quite sure if you wanted me to edit the |
@dotnet-bot test CentOS7.1 x64 Checked Innerloop Build and Test |
src/jit/morph.cpp
Outdated
// Otherwise we may sign-extend incorrectly in cases where the GT_NEG | ||
// node ends up feeding directly into a cast, for example in C# code like | ||
// (byte)(0 - s_1), where s_1 is TYP_UBYTE. | ||
tree->gtOp.gtOp2 = op2 = gtNewOperNode(GT_NEG, genActualType(op2->TypeGet()), op2); |
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.
For consistency, I would prefer something like
for example in
GT_CAST<ubyte>(GT_SUB(0, s_1.ubyte))
src/jit/morph.cpp
Outdated
// Otherwise we may sign-extend incorrectly in cases where the GT_NEG | ||
// node ends up feeding directly a cast, for example in C# code like | ||
// (byte)(-1 * s_1) where s_1 is TYP_UBYTE. | ||
tree->gtOp.gtOp1 = op1 = gtNewOperNode(GT_NEG, genActualType(op1->TypeGet()), op1); |
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.
Similarly here:
for example in
GT_CAST<ubyte>(GT_MUL(-1, s_1.ubyte))
@erozenfeld It should be good now. Thanks for your patience and for teaching me a concise way to describe trees in the JIT! :) |
// The type of the new GT_NEG node cannot just be op1->TypeGet(). | ||
// Otherwise we may sign-extend incorrectly in cases where the GT_NEG | ||
// node ends up feeding directly a cast, for example in | ||
// GT_CAST<ubyte>(GT_MUL(-1, s_1.ubyte) |
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.
Missing parenthesis at the end of the comment but it's not worth it to rerun ci for this. Let's fix that as part of another PR.
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.
Ugh, whoops. I will make sure to fix that the next time I submit something. Thanks for noticing.
@dotnet-bot test OSX10.12 x64 Checked CoreFX Tests |
When the JIT was morphing trees like '-1 * expr', it would turn the
multiplication into a NEG node with the same type as its right operand.
This is a problem when the right operand was a small type like TYP_UBYTE
because the NEG node always produces a widened result. This could cause
problems when the negation was fed into something that depended on the
type, such as a cast to TYP_UBYTE: here the JIT would conclude that the
cast could be dropped, and end up producing a widened result.
The solution is to give the tree the same type as the result of the
original multiplication.
Also add a test for this case and for a similar case of '0 - expr',
which already had a fix.
Fix #18780
No PMI diffs in frameworks or tests except the new test-case for PMI, and no crossgen diffs.