Repository navigation
Ineffective codegen for or pattern type checking #47920
Copy link
Copy link
Closed
Labels
JIT-RBOarea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMItenet-performancePerformance related issuePerformance related issue
Milestone
Description
Description
Examine this code:
using System;
public class C
{
public bool M0(object o)
{
return o is int or uint or long;
}
public bool M1(object o)
{
return o != null && (o is int or uint or long);
}
public bool M2(object o)
{
return o != null && (o.GetType() == typeof(int) || o.GetType() == typeof(uint) || o.GetType() == typeof(long));
}
}IL Compiled:
.method public hidebysig
instance bool M0 (
object o
) cil managed
{
// Method begins at RVA 0x2050
// Code size 28 (0x1c)
.maxstack 8
IL_0000: ldarg.1
IL_0001: isinst [System.Private.CoreLib]System.Int32
IL_0006: brtrue.s IL_001a
IL_0008: ldarg.1
IL_0009: isinst [System.Private.CoreLib]System.UInt32
IL_000e: brtrue.s IL_001a
IL_0010: ldarg.1
IL_0011: isinst [System.Private.CoreLib]System.Int64
IL_0016: ldnull
IL_0017: cgt.un
IL_0019: ret
IL_001a: ldc.i4.1
IL_001b: ret
} // end of method C::M0
.method public hidebysig
instance bool M1 (
object o
) cil managed
{
// Method begins at RVA 0x206d
// Code size 33 (0x21)
.maxstack 8
IL_0000: ldarg.1
IL_0001: brfalse.s IL_001f
IL_0003: ldarg.1
IL_0004: isinst [System.Private.CoreLib]System.Int32
IL_0009: brtrue.s IL_001d
IL_000b: ldarg.1
IL_000c: isinst [System.Private.CoreLib]System.UInt32
IL_0011: brtrue.s IL_001d
IL_0013: ldarg.1
IL_0014: isinst [System.Private.CoreLib]System.Int64
IL_0019: ldnull
IL_001a: cgt.un
IL_001c: ret
IL_001d: ldc.i4.1
IL_001e: ret
IL_001f: ldc.i4.0
IL_0020: ret
} // end of method C::M1
.method public hidebysig
instance bool M2 (
object o
) cil managed
{
// Method begins at RVA 0x2090
// Code size 75 (0x4b)
.maxstack 2
IL_0000: ldarg.1
IL_0001: brfalse.s IL_0049
IL_0003: ldarg.1
IL_0004: callvirt instance class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Object::GetType()
IL_0009: ldtoken [System.Private.CoreLib]System.Int32
IL_000e: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
IL_0013: call bool [System.Private.CoreLib]System.Type::op_Equality(class [System.Private.CoreLib]System.Type, class [System.Private.CoreLib]System.Type)
IL_0018: brtrue.s IL_0047
IL_001a: ldarg.1
IL_001b: callvirt instance class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Object::GetType()
IL_0020: ldtoken [System.Private.CoreLib]System.UInt32
IL_0025: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
IL_002a: call bool [System.Private.CoreLib]System.Type::op_Equality(class [System.Private.CoreLib]System.Type, class [System.Private.CoreLib]System.Type)
IL_002f: brtrue.s IL_0047
IL_0031: ldarg.1
IL_0032: callvirt instance class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Object::GetType()
IL_0037: ldtoken [System.Private.CoreLib]System.Int64
IL_003c: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
IL_0041: call bool [System.Private.CoreLib]System.Type::op_Equality(class [System.Private.CoreLib]System.Type, class [System.Private.CoreLib]System.Type)
IL_0046: ret
IL_0047: ldc.i4.1
IL_0048: ret
IL_0049: ldc.i4.0
IL_004a: ret
} // end of method C::M2codegen:
C.M0(System.Object)
L0000: mov rax, rdx
L0003: test rax, rax
L0006: je short L0017
L0008: mov rcx, 0x7ff9125bb258
L0012: cmp [rax], rcx
L0015: jne short L001c
L0017: test rdx, rdx
L001a: jne short L0058
L001c: mov rax, rdx
L001f: test rax, rax
L0022: je short L0033
L0024: mov rcx, 0x7ff9125bbc18
L002e: cmp [rax], rcx
L0031: jne short L0038
L0033: test rdx, rdx
L0036: jne short L0058
L0038: test rdx, rdx
L003b: je short L004e
L003d: mov rax, 0x7ff9125bc5d8
L0047: cmp [rdx], rax
L004a: je short L004e
L004c: xor edx, edx
L004e: test rdx, rdx
L0051: setne al
L0054: movzx eax, al
L0057: ret
L0058: mov eax, 1
L005d: ret
C.M1(System.Object)
L0000: test rdx, rdx
L0003: je short L004a
L0005: mov rax, rdx
L0008: mov rcx, 0x7ff9125bb258
L0012: cmp [rax], rcx
L0015: je short L0044
L0017: mov rax, rdx
L001a: mov rcx, 0x7ff9125bbc18
L0024: cmp [rax], rcx
L0027: je short L0044
L0029: mov rax, 0x7ff9125bc5d8
L0033: cmp [rdx], rax
L0036: je short L003a
L0038: xor edx, edx
L003a: test rdx, rdx
L003d: setne al
L0040: movzx eax, al
L0043: ret
L0044: mov eax, 1
L0049: ret
L004a: xor eax, eax
L004c: ret
C.M2(System.Object)
L0000: test rdx, rdx
L0003: je short L003d
L0005: mov rax, 0x7ff9125bb258
L000f: cmp [rdx], rax
L0012: je short L0037
L0014: mov rax, 0x7ff9125bbc18
L001e: cmp [rdx], rax
L0021: je short L0037
L0023: mov rax, 0x7ff9125bc5d8
L002d: cmp [rdx], rax
L0030: sete al
L0033: movzx eax, al
L0036: ret
L0037: mov eax, 1
L003c: ret
L003d: xor eax, eax
L003f: retLooking through it:
The pattern matching codegen moves from rdx to rax for every test.
If there's no null check, JIT will generate null test for each condition.
The compiled IL of pattern matching uses ldnull and cgt for the last test. This pattern may confuse the JIT.
Configuration
sharplab default x64 (5.0 timeframe)
Regression?
Probably not.
category:cq
theme:redundant-branches
skill-level:expert
cost:medium
impact:small
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
JIT-RBOarea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMItenet-performancePerformance related issuePerformance related issue
Type
Projects
- StatusShow more project fieldsDone
Activity
AndyAyersMS commentedon Feb 5, 2021
Did you have a chance to look at 6.0 codegen? It may be better.
If not, I'll take a look when I get a chance.
huoyaoyuan commentedon Feb 5, 2021
I changed to
masterbranch in sharplab, and the codegen was identical.Editing the code may crash sharplab.
AndyAyersMS commentedon Feb 5, 2021
I don't think sharplab's
mastertests 6.0 codegen -- suspect it refers to the roslyn branch, not the runtime branch.huoyaoyuan commentedon Feb 5, 2021
Historically, sharplab uses
masterfor both roslyn and runtime master.I've built latest master and viewed with disasmo. The problem still exists.
By the way,
o is int || o is uint || o is longgenerates identical IL with the worst scenario. Roslyn doesn't introduce a new IL pattern for new language feature here.jnyrup commentedon Feb 6, 2021
Here are two other snippets than generates new asm, at least with SharpLab.
I don't know if they are "better", but I observe that:
M3has two instructions less thanM2.M4has two instructions less thanM0, but three more thanM1.sharplab
EgorBo commentedon Feb 10, 2021
Well, even a simple
emits a sub-optimal codegen:
The problem here is the fact we emit a giant tree for the following IL:
After importer:
We emit a huge tree for
isinstwhile not needing its return value (we only compare it with null), we only want something like this:AndyAyersMS commentedon Feb 10, 2021
6.0 codegen for M1 and M2 looks pretty good (better than the sharplab above, which is indeed showing 5.0 codegen).
Main difference from 5.0 is likely the CSE of the method table fetches enabled by #45854. M1 could improve its handling of return values a bit.
AndyAyersMS commentedon Feb 10, 2021
However
M0is still not where it could be.The jump-threading optimizations introduced in #46257 bail out as there is a temp assign (and hence side effect) in the block threading would like to avoid:
This temp assign is consequence of the expansion of qmarks done by morph in
fgExpandQmarkForCastInstOf.At any rate we can / should extend RBO to allow duplication of the side effecting code. I didn't do this initially because:
I don't see us getting to this in 6.0 given other priorities, though this sort of chained type test pattern is likely somewhat common (and more of it is on the way from PGO induced GDV) so we may find time to squeeze it in.
5 remaining items
AndyAyersMS commentedon Oct 1, 2022
Need to test these out with #76476.
huoyaoyuan commentedon Jun 23, 2023
I've investigated myself, and successfully optimized isinst+ldnull+ceq/cgt.un and isinst+brtrue/brfalse patterns locally. However, I have no idea how to handle the isinst+dup+x pattern, which is used by
asfollowed with?.or??.The hard part here is fgExpandQmarkForCastInstOf was written in mind with
asoperator only. I have no knowledge about how qmark nodes can participate in further optimization either.I also tried to introduce a new managed helper for doing the
x is Exactorx as Exactcheck, but found that JIT doesn't allow helper call to be inlined currently.@EgorBo do you have any suggestions around these?
EgorBo commentedon Jun 23, 2023
Not really, ?? and other cases often produce multi-use boxing that we can't pattern match here
it can be implemented, just a bit of work + new JIT-EE API
huoyaoyuan commentedon Jun 23, 2023
I'd like to try it. What information need to be passed in the interface? Eagerly load and pass the body of helper method?
AndyAyersMS commentedon Jun 14, 2024
All of M0-M4 now produce very similar code with 9p5 ish bits (M2 has slightly different layout)
Egor's
Testexample however still has some convoluted flowBut looks like #103391 will address that.
AndyAyersMS commentedon Dec 14, 2024
Testnow looks ok (9.0)but
M1seems to have regressed a bit:AndyAyersMS commentedon Jun 5, 2026
Note
AI-assisted (Copilot CLI).
Re-measured the original M0/M1/M2 and added two complement variants (M3, M4) across .NET 8/9/10/11. The original headline complaint — that the
is X or Y or Zform (M0) produced worse code than the explicitGetType()form (M2) — is resolved.Codegen sizes (arm64, .NET 11, FullOpts, NoInlining)
o is int or uint or longcbz/null +cmp/beq+ccmpcombine for uint+longo != null && (o is int or uint or long)cmp x1,0; cset; cbz w0for the leading!= nullo != null && (o.GetType() == typeof(int) ‖ ...)cmp/beqGetType compares, noccmpo.GetType() == typeof(int) ‖ ...(no null check)cmp/beq, noccmp, no null check (NREs on null)o is int ‖ o is uint ‖ o is long(explicit‖)cmp/beqisinst chain, noccmpKey finding: M0's
or-pattern syntax is what enables theccmpcombine — the equivalent||-chain (M4) produces 17% larger code with three separate compares. So users who already wroteo is X || o is Y || o is Zwould get strictly better codegen by switching too is X or Y or Z.Perf (Apple M4 Max, arm64, BDN 0.16, 5 mixed inputs)
Perf differences are at the limit of measurement noise on this input set. The codegen size ranking (M0 = M3 < M1 < M2 = M4) is more reliable than the absolute ns numbers, which depend heavily on branch-prediction behaviour for whatever input mix the user happens to have. There is a measurable .NET 9 → .NET 10 regression for M0/M2 on this specific input pattern (5 mixed inputs, 60% true) that doesn't appear with other input mixes; could be tracked separately if reproducible elsewhere.
Remaining codegen-quality observations
o != null && (o is X)could fold to justo is X(sinceisinstalready short-circuits null), saving ~8 bytes / 3 instructions on arm64. The leadingcmp x1, #0; cset x0, ne; cbz w0, ...could collapse tocbz x1, .... The bool temp is preserved becauseoptOptimizeBoolsdoesn't simplifySTORE V02 = (o != null); JTRUE V02 == 0when V02 is reassigned later in flow.ccmpcombining. Only theor-pattern path triggers the methodtable-equalityccmpcombine. Extending the same combining to the||-chain forms of isinst and to GetType equality chains would close the gap.Suggest
Closing this as resolved (original "ineffective codegen" gap is fixed in .NET 9+ and M0 now wins on size). Codegen-quality observations 1 and 2 above could be filed as separate follow-ups if desired.
AndyAyersMS commentedon Jun 5, 2026
Closing as resolved per analysis above. Filing remaining codegen-quality observations as a follow-up.
AndyAyersMS commentedon Jun 5, 2026
Follow-up codegen-quality issue filed as #129055.