Skip to content

Commit 483db3e

Browse files
Avoid scanning isinst checks when building whole program view (#104148)
Before this PR, we were somewhat able to eliminate dead typeof checks such as: ```csharp if (someType is Foo f) { ExpensiveMethod(); } ``` This work was done in #99761. However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent. With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them. Despite RyuJIT already doing this optimization, I'm also doing it in the SubstitutedILProvider because this optimization _must_ happen, or we'll get codegen failures. With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
1 parent 018fb9d commit 483db3e

File tree

5 files changed

+227
-4
lines changed

5 files changed

+227
-4
lines changed

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/SubstitutedILProvider.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method)
253253
continue;
254254

255255
TypeEqualityPatternAnalyzer typeEqualityAnalyzer = default;
256+
IsInstCheckPatternAnalyzer isInstCheckAnalyzer = default;
256257

257258
ILReader reader = new ILReader(methodBytes, offset);
258259
while (reader.HasNext)
@@ -262,6 +263,7 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method)
262263
ILOpcode opcode = reader.ReadILOpcode();
263264

264265
typeEqualityAnalyzer.Advance(opcode, reader, method);
266+
isInstCheckAnalyzer.Advance(opcode, reader, method);
265267

266268
// Mark any applicable EH blocks
267269
foreach (ILExceptionRegion ehRegion in ehRegions)
@@ -302,7 +304,8 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method)
302304
{
303305
int destination = reader.ReadBranchDestination(opcode);
304306
if (!TryGetConstantArgument(method, methodBytes, flags, offset, 0, out int constant)
305-
&& !TryExpandTypeEquality(typeEqualityAnalyzer, method, out constant))
307+
&& !TryExpandTypeEquality(typeEqualityAnalyzer, method, out constant)
308+
&& !TryExpandIsInst(isInstCheckAnalyzer, method, out constant))
306309
{
307310
// Can't get the constant - both branches are live.
308311
offsetsToVisit.Push(destination);
@@ -931,6 +934,35 @@ private bool TryExpandTypeEquality(in TypeEqualityPatternAnalyzer analyzer, Meth
931934
return true;
932935
}
933936

937+
private bool TryExpandIsInst(in IsInstCheckPatternAnalyzer analyzer, MethodIL methodIL, out int constant)
938+
{
939+
constant = 0;
940+
if (!analyzer.IsIsInstBranch)
941+
return false;
942+
943+
var type = (TypeDesc)methodIL.GetObject(analyzer.Token);
944+
945+
// Dataflow runs on top of uninstantiated IL and we can't answer some questions there.
946+
// Unfortunately this means dataflow will still see code that the rest of the system
947+
// might have optimized away. It should not be a problem in practice.
948+
if (type.ContainsSignatureVariables())
949+
return false;
950+
951+
if (type.IsCanonicalDefinitionType(CanonicalFormKind.Any))
952+
return false;
953+
954+
// We don't track types without a constructed MethodTable very well.
955+
if (!ConstructedEETypeNode.CreationAllowed(type))
956+
return false;
957+
958+
if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(type.NormalizeInstantiation()))
959+
return false;
960+
961+
constant = 0;
962+
963+
return true;
964+
}
965+
934966
private static TypeDesc ReadLdToken(ref ILReader reader, MethodIL methodIL, OpcodeFlags[] flags)
935967
{
936968
ILOpcode opcode = reader.ReadILOpcode();

src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ internal partial class ILImporter
3535
private readonly byte[] _ilBytes;
3636

3737
private TypeEqualityPatternAnalyzer _typeEqualityPatternAnalyzer;
38+
private IsInstCheckPatternAnalyzer _isInstCheckPatternAnalyzer;
3839

3940
private sealed class BasicBlock
4041
{
@@ -263,11 +264,13 @@ private void StartImportingBasicBlock(BasicBlock basicBlock)
263264
}
264265

265266
_typeEqualityPatternAnalyzer = default;
267+
_isInstCheckPatternAnalyzer = default;
266268
}
267269

268270
partial void StartImportingInstruction(ILOpcode opcode)
269271
{
270272
_typeEqualityPatternAnalyzer.Advance(opcode, new ILReader(_ilBytes, _currentOffset), _methodIL);
273+
_isInstCheckPatternAnalyzer.Advance(opcode, new ILReader(_ilBytes, _currentOffset), _methodIL);
271274
}
272275

273276
private void EndImportingInstruction()
@@ -837,6 +840,16 @@ private void ImportBranch(ILOpcode opcode, BasicBlock target, BasicBlock fallthr
837840
}
838841
}
839842

843+
if (opcode == ILOpcode.brfalse && _isInstCheckPatternAnalyzer.IsIsInstBranch)
844+
{
845+
TypeDesc isinstCheckType = (TypeDesc)_canonMethodIL.GetObject(_isInstCheckPatternAnalyzer.Token);
846+
if (ConstructedEETypeNode.CreationAllowed(isinstCheckType)
847+
&& !isinstCheckType.ConvertToCanonForm(CanonicalFormKind.Specific).IsCanonicalSubtype(CanonicalFormKind.Any))
848+
{
849+
condition = _factory.MaximallyConstructableType(isinstCheckType);
850+
}
851+
}
852+
840853
ImportFallthrough(target);
841854

842855
if (fallthrough != null)
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
using Internal.IL;
7+
using Internal.TypeSystem;
8+
9+
namespace ILCompiler
10+
{
11+
/// <summary>
12+
/// Simple state machine to analyze IL sequences that represent isinst checks.
13+
/// </summary>
14+
internal struct IsInstCheckPatternAnalyzer
15+
{
16+
// Captures following sequence:
17+
//
18+
// isinst Foo
19+
// Optional:
20+
// stloc Y
21+
// ldloc Y
22+
// Optional:
23+
// ldnull
24+
// cgt.un
25+
// Optional:
26+
// stloc X
27+
// ldloc X
28+
// brfalse
29+
private enum State : byte
30+
{
31+
IsInst = 1,
32+
33+
IsInstStLoc,
34+
35+
IsInstLdnull,
36+
IsInstLdnullCgt,
37+
IsInstLdnullCgtStLoc,
38+
IsInstLdnullCgtStLocLdLoc,
39+
40+
Branch,
41+
}
42+
43+
private State _state;
44+
private int _token;
45+
46+
public readonly bool IsIsInstBranch => _state is State.Branch;
47+
public int Token => _token;
48+
49+
public void Advance(ILOpcode opcode, in ILReader reader, MethodIL methodIL)
50+
{
51+
switch (_state)
52+
{
53+
case 0:
54+
if (opcode == ILOpcode.isinst)
55+
(_state, _token) = (State.IsInst, reader.PeekILToken());
56+
return;
57+
case State.IsInst:
58+
if (opcode is ILOpcode.brfalse or ILOpcode.brfalse_s)
59+
_state = State.Branch;
60+
else if (opcode == ILOpcode.ldnull)
61+
_state = State.IsInstLdnull;
62+
else if (IsStlocLdlocSequence(opcode, reader))
63+
_state = State.IsInstStLoc;
64+
else
65+
break;
66+
return;
67+
case State.IsInstLdnull:
68+
if (opcode == ILOpcode.cgt_un)
69+
_state = State.IsInstLdnullCgt;
70+
else
71+
break;
72+
return;
73+
case State.IsInstLdnullCgt:
74+
if (IsStlocLdlocSequence(opcode, reader))
75+
_state = State.IsInstLdnullCgtStLoc;
76+
else
77+
break;
78+
return;
79+
case State.IsInstLdnullCgtStLoc:
80+
if (opcode == ILOpcode.ldloc || opcode == ILOpcode.ldloc_s || (opcode >= ILOpcode.ldloc_0 && opcode <= ILOpcode.ldloc_3))
81+
_state = State.IsInstLdnullCgtStLocLdLoc;
82+
else
83+
throw new UnreachableException();
84+
return;
85+
case State.IsInstLdnullCgtStLocLdLoc:
86+
if (opcode is ILOpcode.brfalse or ILOpcode.brfalse_s)
87+
_state = State.Branch;
88+
else
89+
break;
90+
return;
91+
case State.IsInstStLoc:
92+
if (opcode == ILOpcode.ldloc || opcode == ILOpcode.ldloc_s || (opcode >= ILOpcode.ldloc_0 && opcode <= ILOpcode.ldloc_3))
93+
_state = State.IsInst;
94+
else
95+
throw new UnreachableException();
96+
return;
97+
default:
98+
throw new UnreachableException();
99+
}
100+
101+
_state = default;
102+
103+
static bool IsStlocLdlocSequence(ILOpcode opcode, in ILReader reader)
104+
{
105+
if (opcode == ILOpcode.stloc || opcode == ILOpcode.stloc_s || (opcode >= ILOpcode.stloc_0 && opcode <= ILOpcode.stloc_3))
106+
{
107+
ILReader nestedReader = reader;
108+
int locIndex = opcode switch
109+
{
110+
ILOpcode.stloc => nestedReader.ReadILUInt16(),
111+
ILOpcode.stloc_s => nestedReader.ReadILByte(),
112+
_ => opcode - ILOpcode.stloc_0,
113+
};
114+
ILOpcode otherOpcode = nestedReader.ReadILOpcode();
115+
return (otherOpcode == ILOpcode.ldloc || otherOpcode == ILOpcode.ldloc_s || (otherOpcode >= ILOpcode.ldloc_0 && otherOpcode <= ILOpcode.ldloc_3))
116+
&& otherOpcode switch
117+
{
118+
ILOpcode.ldloc => nestedReader.ReadILUInt16(),
119+
ILOpcode.ldloc_s => nestedReader.ReadILByte(),
120+
_ => otherOpcode - ILOpcode.ldloc_0,
121+
} == locIndex;
122+
}
123+
return false;
124+
}
125+
126+
Advance(opcode, reader, methodIL);
127+
}
128+
}
129+
}

src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,7 @@
669669
<Compile Include="Compiler\Logging\UnconditionalSuppressMessageAttributeState.cs" />
670670
<Compile Include="Compiler\XmlObjectDumper.cs" />
671671
<Compile Include="IL\ILImporter.Scanner.cs" />
672+
<Compile Include="IL\IsInstCheckPatternAnalyzer.cs" />
672673
<Compile Include="IL\TypeEqualityPatternAnalyzer.cs" />
673674
<Compile Include="IL\Stubs\PInvokeILProvider.cs" />
674675
<Compile Include="IL\Stubs\StartupCode\StartupCodeMainMethod.cs" />

src/tests/nativeaot/SmokeTests/TrimmingBehaviors/DeadCodeElimination.cs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static int Run()
2020
TestUnusedDefaultInterfaceMethod.Run();
2121
TestArrayElementTypeOperations.Run();
2222
TestStaticVirtualMethodOptimizations.Run();
23+
TestTypeIs.Run();
2324
TestTypeEquals.Run();
2425
TestTypeEqualityDeadBranchScanRemoval.Run();
2526
TestTypeIsEnum.Run();
@@ -333,6 +334,53 @@ public static void Run()
333334
}
334335
}
335336

337+
338+
class TestTypeIs
339+
{
340+
class Never1 { }
341+
class Canary1 { }
342+
343+
class Maybe1<T, U> { }
344+
345+
[MethodImpl(MethodImplOptions.NoInlining)]
346+
static object GetTheObject() => new object();
347+
348+
static volatile object s_sink;
349+
350+
public static void Run()
351+
{
352+
#if !DEBUG
353+
{
354+
RunCheck(GetTheObject());
355+
356+
static void RunCheck(object t)
357+
{
358+
if (t is Never1)
359+
{
360+
s_sink = new Canary1();
361+
}
362+
}
363+
364+
ThrowIfPresent(typeof(TestTypeIs), nameof(Canary1));
365+
}
366+
367+
{
368+
RunCheck<object>(new Maybe1<object, string>());
369+
370+
[MethodImpl(MethodImplOptions.NoInlining)]
371+
static void RunCheck<T>(object t)
372+
{
373+
if (t is Maybe1<T, string>)
374+
{
375+
return;
376+
}
377+
throw new Exception();
378+
}
379+
}
380+
#endif
381+
}
382+
}
383+
336384
class TestTypeEquals
337385
{
338386
sealed class Gen<T> { }
@@ -382,7 +430,7 @@ static void RunCheck(Type t)
382430
}
383431
}
384432

385-
ThrowIfPresentWithUsableMethodTable(typeof(TestTypeEquals), nameof(Canary2));
433+
ThrowIfPresent(typeof(TestTypeEquals), nameof(Canary2));
386434
}
387435

388436
{
@@ -397,7 +445,7 @@ static void RunCheck(object o)
397445
}
398446
}
399447

400-
ThrowIfPresentWithUsableMethodTable(typeof(TestTypeEquals), nameof(Canary3));
448+
ThrowIfPresent(typeof(TestTypeEquals), nameof(Canary3));
401449
}
402450

403451
{
@@ -412,7 +460,7 @@ static void RunCheck(object o)
412460
}
413461
}
414462

415-
ThrowIfPresentWithUsableMethodTable(typeof(TestTypeEquals), nameof(Canary4));
463+
ThrowIfPresent(typeof(TestTypeEquals), nameof(Canary4));
416464
}
417465

418466
{

0 commit comments

Comments
 (0)