Skip to content

Commit 27ce032

Browse files
authored
Fix quadratic algorithm in CompilerGeneratedState (#3150)
The way this code was supposed to work was that it would scan the compiler- generated type and all its descendants, record each generated type it found, then fill in information for all of the found types. The way it actually worked was that it would scan the descendants, record each generated type, then try to fill in information *for all generated types found in the program*. This is quadratic as you start adding types, as you rescan everything you've added before. The fix is to record just the types from the current pass, and then add them to the larger bag when everything's complete.
1 parent 6794599 commit 27ce032

File tree

2 files changed

+103
-19
lines changed

2 files changed

+103
-19
lines changed

src/linker/Linker.Dataflow/CompilerGeneratedState.cs

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public static bool TryGetStateMachineType (MethodDefinition method, [NotNullWhen
126126

127127
var callGraph = new CompilerGeneratedCallGraph ();
128128
var userDefinedMethods = new HashSet<MethodDefinition> ();
129+
var generatedTypeToTypeArgs = new Dictionary<TypeDefinition, TypeArgumentInfo> ();
129130

130131
void ProcessMethod (MethodDefinition method)
131132
{
@@ -152,14 +153,15 @@ void ProcessMethod (MethodDefinition method)
152153
if (referencedMethod == null)
153154
continue;
154155

156+
// Find calls to state machine constructors that occur outside the type
155157
if (referencedMethod.IsConstructor &&
156158
referencedMethod.DeclaringType is var generatedType &&
157159
// Don't consider calls in the same type, like inside a static constructor
158160
method.DeclaringType != generatedType &&
159161
CompilerGeneratedNames.IsLambdaDisplayClass (generatedType.Name)) {
160162
// fill in null for now, attribute providers will be filled in later
161-
if (!_generatedTypeToTypeArgumentInfo.TryAdd (generatedType, new TypeArgumentInfo (method, null))) {
162-
var alreadyAssociatedMethod = _generatedTypeToTypeArgumentInfo[generatedType].CreatingMethod;
163+
if (!generatedTypeToTypeArgs.TryAdd (generatedType, new TypeArgumentInfo (method, null))) {
164+
var alreadyAssociatedMethod = generatedTypeToTypeArgs[generatedType].CreatingMethod;
163165
_context.LogWarning (new MessageOrigin (method), DiagnosticId.MethodsAreAssociatedWithUserMethod, method.GetDisplayName (), alreadyAssociatedMethod.GetDisplayName (), generatedType.GetDisplayName ());
164166
}
165167
continue;
@@ -189,7 +191,7 @@ referencedMethod.DeclaringType is var generatedType &&
189191
// Don't consider field accesses in the same type, like inside a static constructor
190192
method.DeclaringType != generatedType &&
191193
CompilerGeneratedNames.IsLambdaDisplayClass (generatedType.Name)) {
192-
if (!_generatedTypeToTypeArgumentInfo.TryAdd (generatedType, new TypeArgumentInfo (method, null))) {
194+
if (!generatedTypeToTypeArgs.TryAdd (generatedType, new TypeArgumentInfo (method, null))) {
193195
// It's expected that there may be multiple methods associated with the same static closure environment.
194196
// All of these methods will substitute the same type arguments into the closure environment
195197
// (if it is generic). Don't warn.
@@ -214,7 +216,7 @@ referencedMethod.DeclaringType is var generatedType &&
214216
}
215217
// Already warned above if multiple methods map to the same type
216218
// Fill in null for argument providers now, the real providers will be filled in later
217-
_generatedTypeToTypeArgumentInfo[stateMachineType] = new TypeArgumentInfo (method, null);
219+
generatedTypeToTypeArgs[stateMachineType] = new TypeArgumentInfo (method, null);
218220
}
219221
}
220222

@@ -280,9 +282,17 @@ referencedMethod.DeclaringType is var generatedType &&
280282

281283
// Now that we have instantiating methods fully filled out, walk the generated types and fill in the attribute
282284
// providers
283-
foreach (var generatedType in _generatedTypeToTypeArgumentInfo.Keys) {
284-
if (HasGenericParameters (generatedType))
285-
MapGeneratedTypeTypeParameters (generatedType);
285+
foreach (var generatedType in generatedTypeToTypeArgs.Keys) {
286+
if (HasGenericParameters (generatedType)) {
287+
MapGeneratedTypeTypeParameters (generatedType, generatedTypeToTypeArgs, _context);
288+
// Finally, add resolved type arguments to the cache
289+
var info = generatedTypeToTypeArgs[generatedType];
290+
if (!_generatedTypeToTypeArgumentInfo.TryAdd (generatedType, info)) {
291+
var method = info.CreatingMethod;
292+
var alreadyAssociatedMethod = _generatedTypeToTypeArgumentInfo[generatedType].CreatingMethod;
293+
_context.LogWarning (new MessageOrigin (method), DiagnosticId.MethodsAreAssociatedWithUserMethod, method.GetDisplayName (), alreadyAssociatedMethod.GetDisplayName (), generatedType.GetDisplayName ());
294+
}
295+
}
286296
}
287297

288298
_cachedTypeToCompilerGeneratedMembers.Add (type, compilerGeneratedCallees);
@@ -301,18 +311,42 @@ static bool HasGenericParameters (TypeDefinition typeDef)
301311
return typeDef.GenericParameters.Count > typeDef.DeclaringType.GenericParameters.Count;
302312
}
303313

304-
void MapGeneratedTypeTypeParameters (TypeDefinition generatedType)
314+
/// <summary>
315+
/// Attempts to reverse the process of the compiler's alpha renaming. So if the original code was
316+
/// something like this:
317+
/// <code>
318+
/// void M&lt;T&gt; () {
319+
/// Action a = () => { Console.WriteLine (typeof (T)); };
320+
/// }
321+
/// </code>
322+
/// The compiler will generate a nested class like this:
323+
/// <code>
324+
/// class &lt;&gt;c__DisplayClass0&lt;T&gt; {
325+
/// public void &lt;M&gt;b__0 () {
326+
/// Console.WriteLine (typeof (T));
327+
/// }
328+
/// }
329+
/// </code>
330+
/// The task of this method is to figure out that the type parameter T in the nested class is the same
331+
/// as the type parameter T in the parent method M.
332+
/// <paramref name="generatedTypeToTypeArgs"/> acts as a memoization table to avoid recalculating the
333+
/// mapping multiple times.
334+
/// </summary>
335+
static void MapGeneratedTypeTypeParameters (
336+
TypeDefinition generatedType,
337+
Dictionary<TypeDefinition, TypeArgumentInfo> generatedTypeToTypeArgs,
338+
LinkContext context)
305339
{
306340
Debug.Assert (CompilerGeneratedNames.IsGeneratedType (generatedType.Name));
307341

308-
var typeInfo = _generatedTypeToTypeArgumentInfo[generatedType];
342+
var typeInfo = generatedTypeToTypeArgs[generatedType];
309343
if (typeInfo.OriginalAttributes is not null) {
310344
return;
311345
}
312346
var method = typeInfo.CreatingMethod;
313347
if (method.Body is { } body) {
314348
var typeArgs = new ICustomAttributeProvider[generatedType.GenericParameters.Count];
315-
var typeRef = ScanForInit (generatedType, body);
349+
var typeRef = ScanForInit (generatedType, body, context);
316350
if (typeRef is null) {
317351
return;
318352
}
@@ -334,9 +368,9 @@ void MapGeneratedTypeTypeParameters (TypeDefinition generatedType)
334368
var owningRef = (TypeReference) owner;
335369
if (!CompilerGeneratedNames.IsGeneratedType (owningRef.Name)) {
336370
userAttrs = param;
337-
} else if (_context.TryResolve ((TypeReference) param.Owner) is { } owningType) {
338-
MapGeneratedTypeTypeParameters (owningType);
339-
if (_generatedTypeToTypeArgumentInfo[owningType].OriginalAttributes is { } owningAttrs) {
371+
} else if (context.TryResolve ((TypeReference) param.Owner) is { } owningType) {
372+
MapGeneratedTypeTypeParameters (owningType, generatedTypeToTypeArgs, context);
373+
if (generatedTypeToTypeArgs[owningType].OriginalAttributes is { } owningAttrs) {
340374
userAttrs = owningAttrs[param.Position];
341375
} else {
342376
Debug.Assert (false, "This should be impossible in valid code");
@@ -348,27 +382,30 @@ void MapGeneratedTypeTypeParameters (TypeDefinition generatedType)
348382
typeArgs[i] = userAttrs;
349383
}
350384

351-
_generatedTypeToTypeArgumentInfo[generatedType] = typeInfo with { OriginalAttributes = typeArgs };
385+
generatedTypeToTypeArgs[generatedType] = typeInfo with { OriginalAttributes = typeArgs };
352386
}
353387
}
354388

355-
GenericInstanceType? ScanForInit (TypeDefinition compilerGeneratedType, MethodBody body)
389+
static GenericInstanceType? ScanForInit (
390+
TypeDefinition compilerGeneratedType,
391+
MethodBody body,
392+
LinkContext context)
356393
{
357-
foreach (var instr in _context.GetMethodIL (body).Instructions) {
394+
foreach (var instr in context.GetMethodIL (body).Instructions) {
358395
bool handled = false;
359396
switch (instr.OpCode.Code) {
360397
case Code.Initobj:
361398
case Code.Newobj: {
362399
if (instr.Operand is MethodReference { DeclaringType: GenericInstanceType typeRef }
363-
&& compilerGeneratedType == _context.TryResolve (typeRef)) {
400+
&& compilerGeneratedType == context.TryResolve (typeRef)) {
364401
return typeRef;
365402
}
366403
handled = true;
367404
}
368405
break;
369406
case Code.Stsfld: {
370407
if (instr.Operand is FieldReference { DeclaringType: GenericInstanceType typeRef }
371-
&& compilerGeneratedType == _context.TryResolve (typeRef)) {
408+
&& compilerGeneratedType == context.TryResolve (typeRef)) {
372409
return typeRef;
373410
}
374411
handled = true;
@@ -381,7 +418,7 @@ void MapGeneratedTypeTypeParameters (TypeDefinition generatedType)
381418
if (!handled && instr.OpCode.OperandType is OperandType.InlineMethod) {
382419
if (instr.Operand is GenericInstanceMethod gim) {
383420
foreach (var tr in gim.GenericArguments) {
384-
if (tr is GenericInstanceType git && compilerGeneratedType == _context.TryResolve (git)) {
421+
if (tr is GenericInstanceType git && compilerGeneratedType == context.TryResolve (git)) {
385422
return git;
386423
}
387424
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.IO;
5+
using System.Linq;
6+
7+
/// <summary>
8+
/// This class generates a test that can be used to test perf of analyzing
9+
/// compiler-generated code. Run it by copying this file into a console app and
10+
/// calling <see cref="PerfTestGeneratorForCompilerGeneratedCode.Run"/>. A file
11+
/// will be generated in the current directory named GeneratedLinkerTests.cs.
12+
/// Copy this file into another Console app and trim the app to measure the
13+
/// perf.
14+
/// </summary>
15+
static class PerfTestGeneratorForCompilerGeneratedCode
16+
{
17+
const int FuncNumber = 10000;
18+
public static void Run ()
19+
{
20+
using var fstream = File.Create ("GeneratedLinkerTests.cs");
21+
using var writer = new StreamWriter (fstream);
22+
writer.WriteLine ($$"""
23+
class C {
24+
public static async void Main()
25+
{
26+
int x = 0;
27+
{{string.Join (@"
28+
", Enumerable.Range (0, FuncNumber).Select (i => $"x += await N{i}<int>.M();"))}}
29+
Console.WriteLine(x);
30+
}
31+
}
32+
""");
33+
for (int i = 0; i < FuncNumber; i++) {
34+
writer.WriteLine ($$"""
35+
public static class N{{i}}<T>
36+
{
37+
public static async ValueTask<int> M()
38+
{
39+
Func<int> a = () => 1;
40+
await Task.Delay(0);
41+
return a();
42+
}
43+
}
44+
""");
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)