JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)' - #126070

Closed
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or
Closed

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)'#126070
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or

Conversation

@BoyBaykiller

Copy link
Copy Markdown
Contributor

No description provided.

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 25, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I am having a problem identifying certain cases.
Currently I detect such IR:

[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V02 arg2 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V02 arg2 (last use)
[000005] ----------- \--* CNS_INT int 512

Which works fine for (flags & 256) | (flags & 512),
but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

[000008] ----------- \--* OR int [000004] ----------- +--* ADD int [000000] ----------- | +--* CNS_INT int 1
[000003] ----------- | \--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000007] ----------- \--* AND int [000005] ----------- +--* LCL_VAR int V01 arg1 [000006] ----------- \--* CNS_INT int 512

When adding explicit parentheses like 1 + ((flags & 256) | (flags & 512)) it works again:

[000008] ----------- \--* ADD int [000000] ----------- +--* CNS_INT int 1
[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V01 arg1 [000005] ----------- \--* CNS_INT int 512

But this shouldn't be needed. It shouldnt get confused by some extra commutative arithmetic like the ADD(1) here. What is the general way to fix this?

@huoyaoyuan

Copy link
Copy Markdown
Member

but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
// Fold "(cmp & x) | (cmp & y)" to "cmp & (x | y)".
if (varTypeIsIntegralOrI(orOp) && op1->OperIs(GT_AND) && op2->OperIs(GT_AND))
{
if (GenTree::Compare(op1->gtGetOp1(), op2->gtGetOp1()))

Copy link
Copy Markdown
Member

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 legal - cmp may have side-effect or be a local whose value is changed via x or y

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

of a side effect? just a tree that increments a field. it does it twice, you remove one of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me see

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied a check from fgRecognizeAndMorphBitwiseRotation that should make it safe:

if (((tree->gtFlags & GTF_PERSISTENT_SIDE_EFFECTS) != 0) || ((tree->gtFlags & GTF_ORDER_SIDEEFF) != 0))
{
// We can't do anything if the tree has stores, calls, or volatile reads. Note that we allow// GTF_EXCEPT side effect since any exceptions thrown by the original tree will be thrown by// the transformed tree as well.returnnullptr;
}

This means these trees are no longer transformed which is correct:

intBugPersSideEffects(intflags){intres=(Consume(flags)&256)|(Consume(flags)&512);returnres;}intBugOrderSideeff(refintflags){Consume(flags);return(Volatile.Read(refflags)&256)|(Volatile.Read(refflags)&512);}

Interestingly, I've run superpmi and there wasn't a single case where this fires.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I've run superpmi and there wasn't a single case where this fires.

🤷 the fact that your current PR finds diffs implies there are such patterns, but we just can't do it in morph.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Ah, I misunderstood you. Well, you still need to add the checks

@BoyBaykillerBoyBaykillerMar 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah of course : )
However the check in it's current form miss some useful cases. For example:

intTransformOnInd(refintflags){Consume(flags);// null checkintres=(flags&256)|(flags&512);returnres;}

Here we bail because of GTF_ORDER_SIDEEFF != 0 check. Am I supposed to use a different flag? What I want to know is "can this IND be removed (no null check attached and not volatile)?".

If I knew how to do that then I could also handle this:

intTransformOnInd2(refintflags){intres=(flags&256)|(flags&512);returnres;}

The first load can't be removed because it's also the nullcheck if I understand correctly. But the second still can. So the transformation can still be done. For this case there is the additional problem that GenTree::Compare returns false when the IND flags aren't the same.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

@huoyaoyuan Bad example from me. Say we have foo | (flags & 256) | (flags & 512).
So order of operations is (foo | (flags & 256)) | (flags & 512). But since all | are commutative the compiler should be free to see it as foo | ((flags & 256) | (flags & 512)) and still apply the transformation where applicable: https://godbolt.org/z/KEa9915cM

* add check for GTF_PERSISTENT_SIDE_EFFECTS so we dont remove a tree when we shouldnt (volatile check still missing)
@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I will close this for now as I've encountered 2 general existing issues:

  1. I opened this PR to make progress towards JIT: Optimize some bitwise ops in context of enum flags #125899, however to handle the original pattern we are missing a general function that reorders arithmethic operations if it enable this (or other) transformations:
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10717-L10719
  2. The transformation can't be done if the op2 tree contains volatile loads. To check for that I should use GTF_ORDER_SIDEEFF flag. But this flag is also set in other cases where the transformation can in fact be done. So there seems to be no good way to tell by the flags alone.
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10726-L10729

Fixing these would help existing transformations too. @EgorBo

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BoyBaykiller@huoyaoyuan@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)' - #126070

Closed
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or
Closed

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)'#126070
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or

Conversation

@BoyBaykiller

Copy link
Copy Markdown
Contributor

No description provided.

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 25, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I am having a problem identifying certain cases.
Currently I detect such IR:

[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V02 arg2 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V02 arg2 (last use)
[000005] ----------- \--* CNS_INT int 512

Which works fine for (flags & 256) | (flags & 512),
but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

[000008] ----------- \--* OR int [000004] ----------- +--* ADD int [000000] ----------- | +--* CNS_INT int 1
[000003] ----------- | \--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000007] ----------- \--* AND int [000005] ----------- +--* LCL_VAR int V01 arg1 [000006] ----------- \--* CNS_INT int 512

When adding explicit parentheses like 1 + ((flags & 256) | (flags & 512)) it works again:

[000008] ----------- \--* ADD int [000000] ----------- +--* CNS_INT int 1
[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V01 arg1 [000005] ----------- \--* CNS_INT int 512

But this shouldn't be needed. It shouldnt get confused by some extra commutative arithmetic like the ADD(1) here. What is the general way to fix this?

@huoyaoyuan

Copy link
Copy Markdown
Member

but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
// Fold "(cmp & x) | (cmp & y)" to "cmp & (x | y)".
if (varTypeIsIntegralOrI(orOp) && op1->OperIs(GT_AND) && op2->OperIs(GT_AND))
{
if (GenTree::Compare(op1->gtGetOp1(), op2->gtGetOp1()))

Copy link
Copy Markdown
Member

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 legal - cmp may have side-effect or be a local whose value is changed via x or y

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

of a side effect? just a tree that increments a field. it does it twice, you remove one of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me see

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied a check from fgRecognizeAndMorphBitwiseRotation that should make it safe:

if (((tree->gtFlags & GTF_PERSISTENT_SIDE_EFFECTS) != 0) || ((tree->gtFlags & GTF_ORDER_SIDEEFF) != 0))
{
// We can't do anything if the tree has stores, calls, or volatile reads. Note that we allow// GTF_EXCEPT side effect since any exceptions thrown by the original tree will be thrown by// the transformed tree as well.returnnullptr;
}

This means these trees are no longer transformed which is correct:

intBugPersSideEffects(intflags){intres=(Consume(flags)&256)|(Consume(flags)&512);returnres;}intBugOrderSideeff(refintflags){Consume(flags);return(Volatile.Read(refflags)&256)|(Volatile.Read(refflags)&512);}

Interestingly, I've run superpmi and there wasn't a single case where this fires.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I've run superpmi and there wasn't a single case where this fires.

🤷 the fact that your current PR finds diffs implies there are such patterns, but we just can't do it in morph.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Ah, I misunderstood you. Well, you still need to add the checks

@BoyBaykillerBoyBaykillerMar 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah of course : )
However the check in it's current form miss some useful cases. For example:

intTransformOnInd(refintflags){Consume(flags);// null checkintres=(flags&256)|(flags&512);returnres;}

Here we bail because of GTF_ORDER_SIDEEFF != 0 check. Am I supposed to use a different flag? What I want to know is "can this IND be removed (no null check attached and not volatile)?".

If I knew how to do that then I could also handle this:

intTransformOnInd2(refintflags){intres=(flags&256)|(flags&512);returnres;}

The first load can't be removed because it's also the nullcheck if I understand correctly. But the second still can. So the transformation can still be done. For this case there is the additional problem that GenTree::Compare returns false when the IND flags aren't the same.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

@huoyaoyuan Bad example from me. Say we have foo | (flags & 256) | (flags & 512).
So order of operations is (foo | (flags & 256)) | (flags & 512). But since all | are commutative the compiler should be free to see it as foo | ((flags & 256) | (flags & 512)) and still apply the transformation where applicable: https://godbolt.org/z/KEa9915cM

* add check for GTF_PERSISTENT_SIDE_EFFECTS so we dont remove a tree when we shouldnt (volatile check still missing)
@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I will close this for now as I've encountered 2 general existing issues:

  1. I opened this PR to make progress towards JIT: Optimize some bitwise ops in context of enum flags #125899, however to handle the original pattern we are missing a general function that reorders arithmethic operations if it enable this (or other) transformations:
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10717-L10719
  2. The transformation can't be done if the op2 tree contains volatile loads. To check for that I should use GTF_ORDER_SIDEEFF flag. But this flag is also set in other cases where the transformation can in fact be done. So there seems to be no good way to tell by the flags alone.
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10726-L10729

Fixing these would help existing transformations too. @EgorBo

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BoyBaykiller@huoyaoyuan@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)' - #126070

Closed
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or
Closed

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)'#126070
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or

Conversation

@BoyBaykiller

Copy link
Copy Markdown
Contributor

No description provided.

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 25, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I am having a problem identifying certain cases.
Currently I detect such IR:

[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V02 arg2 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V02 arg2 (last use)
[000005] ----------- \--* CNS_INT int 512

Which works fine for (flags & 256) | (flags & 512),
but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

[000008] ----------- \--* OR int [000004] ----------- +--* ADD int [000000] ----------- | +--* CNS_INT int 1
[000003] ----------- | \--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000007] ----------- \--* AND int [000005] ----------- +--* LCL_VAR int V01 arg1 [000006] ----------- \--* CNS_INT int 512

When adding explicit parentheses like 1 + ((flags & 256) | (flags & 512)) it works again:

[000008] ----------- \--* ADD int [000000] ----------- +--* CNS_INT int 1
[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V01 arg1 [000005] ----------- \--* CNS_INT int 512

But this shouldn't be needed. It shouldnt get confused by some extra commutative arithmetic like the ADD(1) here. What is the general way to fix this?

@huoyaoyuan

Copy link
Copy Markdown
Member

but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
// Fold "(cmp & x) | (cmp & y)" to "cmp & (x | y)".
if (varTypeIsIntegralOrI(orOp) && op1->OperIs(GT_AND) && op2->OperIs(GT_AND))
{
if (GenTree::Compare(op1->gtGetOp1(), op2->gtGetOp1()))

Copy link
Copy Markdown
Member

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 legal - cmp may have side-effect or be a local whose value is changed via x or y

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

of a side effect? just a tree that increments a field. it does it twice, you remove one of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me see

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied a check from fgRecognizeAndMorphBitwiseRotation that should make it safe:

if (((tree->gtFlags & GTF_PERSISTENT_SIDE_EFFECTS) != 0) || ((tree->gtFlags & GTF_ORDER_SIDEEFF) != 0))
{
// We can't do anything if the tree has stores, calls, or volatile reads. Note that we allow// GTF_EXCEPT side effect since any exceptions thrown by the original tree will be thrown by// the transformed tree as well.returnnullptr;
}

This means these trees are no longer transformed which is correct:

intBugPersSideEffects(intflags){intres=(Consume(flags)&256)|(Consume(flags)&512);returnres;}intBugOrderSideeff(refintflags){Consume(flags);return(Volatile.Read(refflags)&256)|(Volatile.Read(refflags)&512);}

Interestingly, I've run superpmi and there wasn't a single case where this fires.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I've run superpmi and there wasn't a single case where this fires.

🤷 the fact that your current PR finds diffs implies there are such patterns, but we just can't do it in morph.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Ah, I misunderstood you. Well, you still need to add the checks

@BoyBaykillerBoyBaykillerMar 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah of course : )
However the check in it's current form miss some useful cases. For example:

intTransformOnInd(refintflags){Consume(flags);// null checkintres=(flags&256)|(flags&512);returnres;}

Here we bail because of GTF_ORDER_SIDEEFF != 0 check. Am I supposed to use a different flag? What I want to know is "can this IND be removed (no null check attached and not volatile)?".

If I knew how to do that then I could also handle this:

intTransformOnInd2(refintflags){intres=(flags&256)|(flags&512);returnres;}

The first load can't be removed because it's also the nullcheck if I understand correctly. But the second still can. So the transformation can still be done. For this case there is the additional problem that GenTree::Compare returns false when the IND flags aren't the same.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

@huoyaoyuan Bad example from me. Say we have foo | (flags & 256) | (flags & 512).
So order of operations is (foo | (flags & 256)) | (flags & 512). But since all | are commutative the compiler should be free to see it as foo | ((flags & 256) | (flags & 512)) and still apply the transformation where applicable: https://godbolt.org/z/KEa9915cM

* add check for GTF_PERSISTENT_SIDE_EFFECTS so we dont remove a tree when we shouldnt (volatile check still missing)
@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I will close this for now as I've encountered 2 general existing issues:

  1. I opened this PR to make progress towards JIT: Optimize some bitwise ops in context of enum flags #125899, however to handle the original pattern we are missing a general function that reorders arithmethic operations if it enable this (or other) transformations:
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10717-L10719
  2. The transformation can't be done if the op2 tree contains volatile loads. To check for that I should use GTF_ORDER_SIDEEFF flag. But this flag is also set in other cases where the transformation can in fact be done. So there seems to be no good way to tell by the flags alone.
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10726-L10729

Fixing these would help existing transformations too. @EgorBo

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BoyBaykiller@huoyaoyuan@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)' - #126070

Closed
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or
Closed

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)'#126070
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or

Conversation

@BoyBaykiller

Copy link
Copy Markdown
Contributor

No description provided.

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 25, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I am having a problem identifying certain cases.
Currently I detect such IR:

[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V02 arg2 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V02 arg2 (last use)
[000005] ----------- \--* CNS_INT int 512

Which works fine for (flags & 256) | (flags & 512),
but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

[000008] ----------- \--* OR int [000004] ----------- +--* ADD int [000000] ----------- | +--* CNS_INT int 1
[000003] ----------- | \--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000007] ----------- \--* AND int [000005] ----------- +--* LCL_VAR int V01 arg1 [000006] ----------- \--* CNS_INT int 512

When adding explicit parentheses like 1 + ((flags & 256) | (flags & 512)) it works again:

[000008] ----------- \--* ADD int [000000] ----------- +--* CNS_INT int 1
[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V01 arg1 [000005] ----------- \--* CNS_INT int 512

But this shouldn't be needed. It shouldnt get confused by some extra commutative arithmetic like the ADD(1) here. What is the general way to fix this?

@huoyaoyuan

Copy link
Copy Markdown
Member

but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
// Fold "(cmp & x) | (cmp & y)" to "cmp & (x | y)".
if (varTypeIsIntegralOrI(orOp) && op1->OperIs(GT_AND) && op2->OperIs(GT_AND))
{
if (GenTree::Compare(op1->gtGetOp1(), op2->gtGetOp1()))

Copy link
Copy Markdown
Member

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 legal - cmp may have side-effect or be a local whose value is changed via x or y

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

of a side effect? just a tree that increments a field. it does it twice, you remove one of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me see

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied a check from fgRecognizeAndMorphBitwiseRotation that should make it safe:

if (((tree->gtFlags & GTF_PERSISTENT_SIDE_EFFECTS) != 0) || ((tree->gtFlags & GTF_ORDER_SIDEEFF) != 0))
{
// We can't do anything if the tree has stores, calls, or volatile reads. Note that we allow// GTF_EXCEPT side effect since any exceptions thrown by the original tree will be thrown by// the transformed tree as well.returnnullptr;
}

This means these trees are no longer transformed which is correct:

intBugPersSideEffects(intflags){intres=(Consume(flags)&256)|(Consume(flags)&512);returnres;}intBugOrderSideeff(refintflags){Consume(flags);return(Volatile.Read(refflags)&256)|(Volatile.Read(refflags)&512);}

Interestingly, I've run superpmi and there wasn't a single case where this fires.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I've run superpmi and there wasn't a single case where this fires.

🤷 the fact that your current PR finds diffs implies there are such patterns, but we just can't do it in morph.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Ah, I misunderstood you. Well, you still need to add the checks

@BoyBaykillerBoyBaykillerMar 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah of course : )
However the check in it's current form miss some useful cases. For example:

intTransformOnInd(refintflags){Consume(flags);// null checkintres=(flags&256)|(flags&512);returnres;}

Here we bail because of GTF_ORDER_SIDEEFF != 0 check. Am I supposed to use a different flag? What I want to know is "can this IND be removed (no null check attached and not volatile)?".

If I knew how to do that then I could also handle this:

intTransformOnInd2(refintflags){intres=(flags&256)|(flags&512);returnres;}

The first load can't be removed because it's also the nullcheck if I understand correctly. But the second still can. So the transformation can still be done. For this case there is the additional problem that GenTree::Compare returns false when the IND flags aren't the same.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

@huoyaoyuan Bad example from me. Say we have foo | (flags & 256) | (flags & 512).
So order of operations is (foo | (flags & 256)) | (flags & 512). But since all | are commutative the compiler should be free to see it as foo | ((flags & 256) | (flags & 512)) and still apply the transformation where applicable: https://godbolt.org/z/KEa9915cM

* add check for GTF_PERSISTENT_SIDE_EFFECTS so we dont remove a tree when we shouldnt (volatile check still missing)
@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I will close this for now as I've encountered 2 general existing issues:

  1. I opened this PR to make progress towards JIT: Optimize some bitwise ops in context of enum flags #125899, however to handle the original pattern we are missing a general function that reorders arithmethic operations if it enable this (or other) transformations:
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10717-L10719
  2. The transformation can't be done if the op2 tree contains volatile loads. To check for that I should use GTF_ORDER_SIDEEFF flag. But this flag is also set in other cases where the transformation can in fact be done. So there seems to be no good way to tell by the flags alone.
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10726-L10729

Fixing these would help existing transformations too. @EgorBo

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BoyBaykiller@huoyaoyuan@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)' - #126070

Closed
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or
Closed

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)'#126070
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or

Conversation

@BoyBaykiller

Copy link
Copy Markdown
Contributor

No description provided.

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 25, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I am having a problem identifying certain cases.
Currently I detect such IR:

[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V02 arg2 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V02 arg2 (last use)
[000005] ----------- \--* CNS_INT int 512

Which works fine for (flags & 256) | (flags & 512),
but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

[000008] ----------- \--* OR int [000004] ----------- +--* ADD int [000000] ----------- | +--* CNS_INT int 1
[000003] ----------- | \--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000007] ----------- \--* AND int [000005] ----------- +--* LCL_VAR int V01 arg1 [000006] ----------- \--* CNS_INT int 512

When adding explicit parentheses like 1 + ((flags & 256) | (flags & 512)) it works again:

[000008] ----------- \--* ADD int [000000] ----------- +--* CNS_INT int 1
[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V01 arg1 [000005] ----------- \--* CNS_INT int 512

But this shouldn't be needed. It shouldnt get confused by some extra commutative arithmetic like the ADD(1) here. What is the general way to fix this?

@huoyaoyuan

Copy link
Copy Markdown
Member

but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
// Fold "(cmp & x) | (cmp & y)" to "cmp & (x | y)".
if (varTypeIsIntegralOrI(orOp) && op1->OperIs(GT_AND) && op2->OperIs(GT_AND))
{
if (GenTree::Compare(op1->gtGetOp1(), op2->gtGetOp1()))

Copy link
Copy Markdown
Member

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 legal - cmp may have side-effect or be a local whose value is changed via x or y

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

of a side effect? just a tree that increments a field. it does it twice, you remove one of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me see

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied a check from fgRecognizeAndMorphBitwiseRotation that should make it safe:

if (((tree->gtFlags & GTF_PERSISTENT_SIDE_EFFECTS) != 0) || ((tree->gtFlags & GTF_ORDER_SIDEEFF) != 0))
{
// We can't do anything if the tree has stores, calls, or volatile reads. Note that we allow// GTF_EXCEPT side effect since any exceptions thrown by the original tree will be thrown by// the transformed tree as well.returnnullptr;
}

This means these trees are no longer transformed which is correct:

intBugPersSideEffects(intflags){intres=(Consume(flags)&256)|(Consume(flags)&512);returnres;}intBugOrderSideeff(refintflags){Consume(flags);return(Volatile.Read(refflags)&256)|(Volatile.Read(refflags)&512);}

Interestingly, I've run superpmi and there wasn't a single case where this fires.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I've run superpmi and there wasn't a single case where this fires.

🤷 the fact that your current PR finds diffs implies there are such patterns, but we just can't do it in morph.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Ah, I misunderstood you. Well, you still need to add the checks

@BoyBaykillerBoyBaykillerMar 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah of course : )
However the check in it's current form miss some useful cases. For example:

intTransformOnInd(refintflags){Consume(flags);// null checkintres=(flags&256)|(flags&512);returnres;}

Here we bail because of GTF_ORDER_SIDEEFF != 0 check. Am I supposed to use a different flag? What I want to know is "can this IND be removed (no null check attached and not volatile)?".

If I knew how to do that then I could also handle this:

intTransformOnInd2(refintflags){intres=(flags&256)|(flags&512);returnres;}

The first load can't be removed because it's also the nullcheck if I understand correctly. But the second still can. So the transformation can still be done. For this case there is the additional problem that GenTree::Compare returns false when the IND flags aren't the same.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

@huoyaoyuan Bad example from me. Say we have foo | (flags & 256) | (flags & 512).
So order of operations is (foo | (flags & 256)) | (flags & 512). But since all | are commutative the compiler should be free to see it as foo | ((flags & 256) | (flags & 512)) and still apply the transformation where applicable: https://godbolt.org/z/KEa9915cM

* add check for GTF_PERSISTENT_SIDE_EFFECTS so we dont remove a tree when we shouldnt (volatile check still missing)
@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I will close this for now as I've encountered 2 general existing issues:

  1. I opened this PR to make progress towards JIT: Optimize some bitwise ops in context of enum flags #125899, however to handle the original pattern we are missing a general function that reorders arithmethic operations if it enable this (or other) transformations:
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10717-L10719
  2. The transformation can't be done if the op2 tree contains volatile loads. To check for that I should use GTF_ORDER_SIDEEFF flag. But this flag is also set in other cases where the transformation can in fact be done. So there seems to be no good way to tell by the flags alone.
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10726-L10729

Fixing these would help existing transformations too. @EgorBo

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BoyBaykiller@huoyaoyuan@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)' - #126070

Closed
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or
Closed

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)'#126070
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or

Conversation

@BoyBaykiller

Copy link
Copy Markdown
Contributor

No description provided.

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 25, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I am having a problem identifying certain cases.
Currently I detect such IR:

[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V02 arg2 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V02 arg2 (last use)
[000005] ----------- \--* CNS_INT int 512

Which works fine for (flags & 256) | (flags & 512),
but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

[000008] ----------- \--* OR int [000004] ----------- +--* ADD int [000000] ----------- | +--* CNS_INT int 1
[000003] ----------- | \--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000007] ----------- \--* AND int [000005] ----------- +--* LCL_VAR int V01 arg1 [000006] ----------- \--* CNS_INT int 512

When adding explicit parentheses like 1 + ((flags & 256) | (flags & 512)) it works again:

[000008] ----------- \--* ADD int [000000] ----------- +--* CNS_INT int 1
[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V01 arg1 [000005] ----------- \--* CNS_INT int 512

But this shouldn't be needed. It shouldnt get confused by some extra commutative arithmetic like the ADD(1) here. What is the general way to fix this?

@huoyaoyuan

Copy link
Copy Markdown
Member

but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
// Fold "(cmp & x) | (cmp & y)" to "cmp & (x | y)".
if (varTypeIsIntegralOrI(orOp) && op1->OperIs(GT_AND) && op2->OperIs(GT_AND))
{
if (GenTree::Compare(op1->gtGetOp1(), op2->gtGetOp1()))

Copy link
Copy Markdown
Member

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 legal - cmp may have side-effect or be a local whose value is changed via x or y

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

of a side effect? just a tree that increments a field. it does it twice, you remove one of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me see

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied a check from fgRecognizeAndMorphBitwiseRotation that should make it safe:

if (((tree->gtFlags & GTF_PERSISTENT_SIDE_EFFECTS) != 0) || ((tree->gtFlags & GTF_ORDER_SIDEEFF) != 0))
{
// We can't do anything if the tree has stores, calls, or volatile reads. Note that we allow// GTF_EXCEPT side effect since any exceptions thrown by the original tree will be thrown by// the transformed tree as well.returnnullptr;
}

This means these trees are no longer transformed which is correct:

intBugPersSideEffects(intflags){intres=(Consume(flags)&256)|(Consume(flags)&512);returnres;}intBugOrderSideeff(refintflags){Consume(flags);return(Volatile.Read(refflags)&256)|(Volatile.Read(refflags)&512);}

Interestingly, I've run superpmi and there wasn't a single case where this fires.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I've run superpmi and there wasn't a single case where this fires.

🤷 the fact that your current PR finds diffs implies there are such patterns, but we just can't do it in morph.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Ah, I misunderstood you. Well, you still need to add the checks

@BoyBaykillerBoyBaykillerMar 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah of course : )
However the check in it's current form miss some useful cases. For example:

intTransformOnInd(refintflags){Consume(flags);// null checkintres=(flags&256)|(flags&512);returnres;}

Here we bail because of GTF_ORDER_SIDEEFF != 0 check. Am I supposed to use a different flag? What I want to know is "can this IND be removed (no null check attached and not volatile)?".

If I knew how to do that then I could also handle this:

intTransformOnInd2(refintflags){intres=(flags&256)|(flags&512);returnres;}

The first load can't be removed because it's also the nullcheck if I understand correctly. But the second still can. So the transformation can still be done. For this case there is the additional problem that GenTree::Compare returns false when the IND flags aren't the same.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

@huoyaoyuan Bad example from me. Say we have foo | (flags & 256) | (flags & 512).
So order of operations is (foo | (flags & 256)) | (flags & 512). But since all | are commutative the compiler should be free to see it as foo | ((flags & 256) | (flags & 512)) and still apply the transformation where applicable: https://godbolt.org/z/KEa9915cM

* add check for GTF_PERSISTENT_SIDE_EFFECTS so we dont remove a tree when we shouldnt (volatile check still missing)
@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I will close this for now as I've encountered 2 general existing issues:

  1. I opened this PR to make progress towards JIT: Optimize some bitwise ops in context of enum flags #125899, however to handle the original pattern we are missing a general function that reorders arithmethic operations if it enable this (or other) transformations:
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10717-L10719
  2. The transformation can't be done if the op2 tree contains volatile loads. To check for that I should use GTF_ORDER_SIDEEFF flag. But this flag is also set in other cases where the transformation can in fact be done. So there seems to be no good way to tell by the flags alone.
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10726-L10729

Fixing these would help existing transformations too. @EgorBo

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BoyBaykiller@huoyaoyuan@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)' - #126070

Closed
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or
Closed

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)'#126070
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or

Conversation

@BoyBaykiller

Copy link
Copy Markdown
Contributor

No description provided.

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 25, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I am having a problem identifying certain cases.
Currently I detect such IR:

[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V02 arg2 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V02 arg2 (last use)
[000005] ----------- \--* CNS_INT int 512

Which works fine for (flags & 256) | (flags & 512),
but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

[000008] ----------- \--* OR int [000004] ----------- +--* ADD int [000000] ----------- | +--* CNS_INT int 1
[000003] ----------- | \--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000007] ----------- \--* AND int [000005] ----------- +--* LCL_VAR int V01 arg1 [000006] ----------- \--* CNS_INT int 512

When adding explicit parentheses like 1 + ((flags & 256) | (flags & 512)) it works again:

[000008] ----------- \--* ADD int [000000] ----------- +--* CNS_INT int 1
[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V01 arg1 [000005] ----------- \--* CNS_INT int 512

But this shouldn't be needed. It shouldnt get confused by some extra commutative arithmetic like the ADD(1) here. What is the general way to fix this?

@huoyaoyuan

Copy link
Copy Markdown
Member

but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
// Fold "(cmp & x) | (cmp & y)" to "cmp & (x | y)".
if (varTypeIsIntegralOrI(orOp) && op1->OperIs(GT_AND) && op2->OperIs(GT_AND))
{
if (GenTree::Compare(op1->gtGetOp1(), op2->gtGetOp1()))

Copy link
Copy Markdown
Member

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 legal - cmp may have side-effect or be a local whose value is changed via x or y

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

of a side effect? just a tree that increments a field. it does it twice, you remove one of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me see

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied a check from fgRecognizeAndMorphBitwiseRotation that should make it safe:

if (((tree->gtFlags & GTF_PERSISTENT_SIDE_EFFECTS) != 0) || ((tree->gtFlags & GTF_ORDER_SIDEEFF) != 0))
{
// We can't do anything if the tree has stores, calls, or volatile reads. Note that we allow// GTF_EXCEPT side effect since any exceptions thrown by the original tree will be thrown by// the transformed tree as well.returnnullptr;
}

This means these trees are no longer transformed which is correct:

intBugPersSideEffects(intflags){intres=(Consume(flags)&256)|(Consume(flags)&512);returnres;}intBugOrderSideeff(refintflags){Consume(flags);return(Volatile.Read(refflags)&256)|(Volatile.Read(refflags)&512);}

Interestingly, I've run superpmi and there wasn't a single case where this fires.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I've run superpmi and there wasn't a single case where this fires.

🤷 the fact that your current PR finds diffs implies there are such patterns, but we just can't do it in morph.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Ah, I misunderstood you. Well, you still need to add the checks

@BoyBaykillerBoyBaykillerMar 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah of course : )
However the check in it's current form miss some useful cases. For example:

intTransformOnInd(refintflags){Consume(flags);// null checkintres=(flags&256)|(flags&512);returnres;}

Here we bail because of GTF_ORDER_SIDEEFF != 0 check. Am I supposed to use a different flag? What I want to know is "can this IND be removed (no null check attached and not volatile)?".

If I knew how to do that then I could also handle this:

intTransformOnInd2(refintflags){intres=(flags&256)|(flags&512);returnres;}

The first load can't be removed because it's also the nullcheck if I understand correctly. But the second still can. So the transformation can still be done. For this case there is the additional problem that GenTree::Compare returns false when the IND flags aren't the same.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

@huoyaoyuan Bad example from me. Say we have foo | (flags & 256) | (flags & 512).
So order of operations is (foo | (flags & 256)) | (flags & 512). But since all | are commutative the compiler should be free to see it as foo | ((flags & 256) | (flags & 512)) and still apply the transformation where applicable: https://godbolt.org/z/KEa9915cM

* add check for GTF_PERSISTENT_SIDE_EFFECTS so we dont remove a tree when we shouldnt (volatile check still missing)
@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I will close this for now as I've encountered 2 general existing issues:

  1. I opened this PR to make progress towards JIT: Optimize some bitwise ops in context of enum flags #125899, however to handle the original pattern we are missing a general function that reorders arithmethic operations if it enable this (or other) transformations:
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10717-L10719
  2. The transformation can't be done if the op2 tree contains volatile loads. To check for that I should use GTF_ORDER_SIDEEFF flag. But this flag is also set in other cases where the transformation can in fact be done. So there seems to be no good way to tell by the flags alone.
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10726-L10729

Fixing these would help existing transformations too. @EgorBo

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BoyBaykiller@huoyaoyuan@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)' - #126070

Closed
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or
Closed

JIT: Transform '(cmp & x) | (cmp & y)' to 'cmp & (x | y)'#126070
BoyBaykiller wants to merge 3 commits into
dotnet:mainfrom
BoyBaykiller:transform-double-and-or

Conversation

@BoyBaykiller

Copy link
Copy Markdown
Contributor

No description provided.

@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 25, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I am having a problem identifying certain cases.
Currently I detect such IR:

[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V02 arg2 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V02 arg2 (last use)
[000005] ----------- \--* CNS_INT int 512

Which works fine for (flags & 256) | (flags & 512),
but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

[000008] ----------- \--* OR int [000004] ----------- +--* ADD int [000000] ----------- | +--* CNS_INT int 1
[000003] ----------- | \--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000007] ----------- \--* AND int [000005] ----------- +--* LCL_VAR int V01 arg1 [000006] ----------- \--* CNS_INT int 512

When adding explicit parentheses like 1 + ((flags & 256) | (flags & 512)) it works again:

[000008] ----------- \--* ADD int [000000] ----------- +--* CNS_INT int 1
[000007] ----------- \--* OR int [000003] ----------- +--* AND int [000001] ----------- | +--* LCL_VAR int V01 arg1 [000002] ----------- | \--* CNS_INT int 256
[000006] ----------- \--* AND int [000004] ----------- +--* LCL_VAR int V01 arg1 [000005] ----------- \--* CNS_INT int 512

But this shouldn't be needed. It shouldnt get confused by some extra commutative arithmetic like the ADD(1) here. What is the general way to fix this?

@huoyaoyuan

Copy link
Copy Markdown
Member

but breaks down if I add a operation in front like 1 + (flags & 256) | (flags & 512). Then the IR is:

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
// Fold "(cmp & x) | (cmp & y)" to "cmp & (x | y)".
if (varTypeIsIntegralOrI(orOp) && op1->OperIs(GT_AND) && op2->OperIs(GT_AND))
{
if (GenTree::Compare(op1->gtGetOp1(), op2->gtGetOp1()))

Copy link
Copy Markdown
Member

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 legal - cmp may have side-effect or be a local whose value is changed via x or y

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give am example?

of a side effect? just a tree that increments a field. it does it twice, you remove one of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let me see

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied a check from fgRecognizeAndMorphBitwiseRotation that should make it safe:

if (((tree->gtFlags & GTF_PERSISTENT_SIDE_EFFECTS) != 0) || ((tree->gtFlags & GTF_ORDER_SIDEEFF) != 0))
{
// We can't do anything if the tree has stores, calls, or volatile reads. Note that we allow// GTF_EXCEPT side effect since any exceptions thrown by the original tree will be thrown by// the transformed tree as well.returnnullptr;
}

This means these trees are no longer transformed which is correct:

intBugPersSideEffects(intflags){intres=(Consume(flags)&256)|(Consume(flags)&512);returnres;}intBugOrderSideeff(refintflags){Consume(flags);return(Volatile.Read(refflags)&256)|(Volatile.Read(refflags)&512);}

Interestingly, I've run superpmi and there wasn't a single case where this fires.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I've run superpmi and there wasn't a single case where this fires.

🤷 the fact that your current PR finds diffs implies there are such patterns, but we just can't do it in morph.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the PERSISTENT_SIDE_EFFECTS/ORDER_SIDEEFF correctnes check doesnt result in any additional diffs compared to not having this correctnes check. I've tested this by putting assert(false) in it and running replay.

Ah, I misunderstood you. Well, you still need to add the checks

@BoyBaykillerBoyBaykillerMar 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah of course : )
However the check in it's current form miss some useful cases. For example:

intTransformOnInd(refintflags){Consume(flags);// null checkintres=(flags&256)|(flags&512);returnres;}

Here we bail because of GTF_ORDER_SIDEEFF != 0 check. Am I supposed to use a different flag? What I want to know is "can this IND be removed (no null check attached and not volatile)?".

If I knew how to do that then I could also handle this:

intTransformOnInd2(refintflags){intres=(flags&256)|(flags&512);returnres;}

The first load can't be removed because it's also the nullcheck if I understand correctly. But the second still can. So the transformation can still be done. For this case there is the additional problem that GenTree::Compare returns false when the IND flags aren't the same.

@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

The operator priority of | is lower than +. 1 + (flags & 256) | (flags & 512) means (1 + (flags & 256)) | (flags & 512). Check sharplab.

@huoyaoyuan Bad example from me. Say we have foo | (flags & 256) | (flags & 512).
So order of operations is (foo | (flags & 256)) | (flags & 512). But since all | are commutative the compiler should be free to see it as foo | ((flags & 256) | (flags & 512)) and still apply the transformation where applicable: https://godbolt.org/z/KEa9915cM

* add check for GTF_PERSISTENT_SIDE_EFFECTS so we dont remove a tree when we shouldnt (volatile check still missing)
@BoyBaykiller

Copy link
Copy Markdown
ContributorAuthor

I will close this for now as I've encountered 2 general existing issues:

  1. I opened this PR to make progress towards JIT: Optimize some bitwise ops in context of enum flags #125899, however to handle the original pattern we are missing a general function that reorders arithmethic operations if it enable this (or other) transformations:
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10717-L10719
  2. The transformation can't be done if the op2 tree contains volatile loads. To check for that I should use GTF_ORDER_SIDEEFF flag. But this flag is also set in other cases where the transformation can in fact be done. So there seems to be no good way to tell by the flags alone.
    https://github.com/BoyBaykiller/runtime/blob/205c820fb048066d4f64ac5996d9ecdd7260c864/src/coreclr/jit/morph.cpp#L10726-L10729

Fixing these would help existing transformations too. @EgorBo

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BoyBaykiller@huoyaoyuan@EgorBo