JIT: extend ccmp combining and fold redundant null-check for isinst / GetType chains
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
> [!NOTE]
> AI-assisted (Copilot CLI).
Follow-up to #47920 (closed as resolved). The original "ineffective codegen" complaint is fixed — `o is int or uint or long` (M0) now produces the smallest, fastest code, using arm64 `ccmp` to combine the second and third type-id compares. Two related codegen-quality gaps remain:
### 1. Redundant null-check materialization when `o != null && (o is X)` appears
```csharp
bool M1(object o) => o != null && (o is int or uint or long);
```
The leading `o != null` is semantically redundant (the isinst already returns `false` for null), but the JIT emits:
```
cmp x1, #0 ; <-- could be folded away
cset x0, ne
cbz w0, IG06
ldr x0, [x1] ; ...isinst chain identical to M0...
```
Equivalent C# without the leading null check (M0) emits just `cbz x1, IG05` instead. ~8 bytes / 3 instructions wasted per call. The IL pattern is `cgt.un(o, null); stloc V02; ldloc V02; brfalse`. `optOptimizeBools` could simplify `STORE V02 = (X != null); JTRUE V02 == 0` into `JTRUE X == null` (and forward-substitute to eliminate V02's first definition), but it doesn't, likely because V02 is reassigned later in flow with the isinst result.
### 2. `ccmp` combining only fires for the `or`-pattern, not for `||`-chains or `GetType()`-equality chains
| Form | Code size (arm64 .NET 11) | Uses `ccmp` |
|------|---:|---|
| `o is int or uint or long` | **92 B** | yes (combines uint+long) |
| `o is int \|\| o is uint \|\| o is long` | 108 B | no — three separate `cmp/beq` |
| `o.GetType() == typeof(int) \|\| o.GetType() == typeof(uint) \|\| o.GetType() == typeof(long)` | 92 B (without null check) / 108 B (with) | no — three separate `cmp/beq` |
The `||`-chain pattern is the obvious hand-written form; users with existing code should already get the same `ccmp` combining the `or`-pattern enjoys. Likewise the explicit `GetType() == typeof(X) || ...` chain (a common pattern from before `is`-patterns existed) should benefit.
### Suggested next steps
- Investigate generalizing the `ccmp` combine in arm64 lowering to handle short `||`-chains of methodtable-equality comparisons, regardless of whether the source pattern came from `is X or Y or Z`, `is X || is Y || is Z`, or `GetType() == typeof(X) || ...`.
- Fix `optOptimizeBools` / value-numbering to fold `(X != null) && IsInst(X, T)` → `IsInst(X, T)` (or equivalently fold the redundant null-materialization).
Related: #47920 (closed).
cc @dotnet/jit-contrib
Contributor guide
Assessment
This issue has not been assessed yet.