Skip to content

Commit cc3c5bc

Browse files
authored
Merge pull request #40087 from dotnet/features/localsinit
Merge features/localsinit into master
2 parents 801184c + a0387ca commit cc3c5bc

File tree

92 files changed

+3150
-64
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+3150
-64
lines changed

docs/contributing/Compiler Test Plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ This document provides guidance for thinking about language interactions and tes
6262
- Dynamic
6363
- Ref structs, Readonly structs
6464
- Readonly members on structs (methods, property/indexer accessors, custom event accessors)
65+
- SkipLocalsInit
6566

6667
# Code
6768
- Operators (see Eric's list below)

docs/features/skiplocalsinit.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
# Feature design for SkipLocalsInit
3+
4+
The feature is to add a new well-known attribute to the compiler, System.Runtime.CompilerServices.SkipLocalsInit that causes method bodies nested "inside" the scope of the attribute to elide the ".locals init" CIL directive that causes the CLR to zero-init local variables and space reserved using the "localloc" instruction. "Nested inside the scope of the attribute" is a concept defined as follows:
5+
6+
1. When the attribute is applied to a module, all emitted methods inside that module, including generated methods, will skip local initialization.
7+
8+
2. When the attribute is applied to a type (including interfaces), all methods transitively inside that type, including generated methods, will skip local initialization. This includes nested types.
9+
10+
3. When the attribute is applied to a method, the method and all methods generated by the compiler which contain user-influenced code inside that method (e.g., local functions, lambdas, async methods) will skip local initialization.
11+
12+
For the above, generated methods will skip local initialization only if they have user code inside them, or a significant number of
13+
compiler-generated locals. For example, the MoveNext method of an async method will skip local initialization, but the constructor of
14+
a display class probably will not, as the display class constructor likely has no locals anyway.
15+
16+
Some notable decisions:
17+
18+
1. Although SkipLocalsInit does not require unsafe code blocks, it does require the `unsafe` flag to be passed to the compiler. This
19+
is to signal that in some cases code could view unassigned memory, including `stackalloc`.
20+
21+
2. Due to the compiler not controlling localsinit for other modules when emitting to a netmodule, SkipLocalsInit is not respected on
22+
the assembly level, even if the AttributeUsage of the SkipLocalsInitAttribute is specified that it is allowed.
23+
24+
3. The `Inherited` property of the SkipLocalsInitAttribute is not respected. SkipLocalsInit does not propagate through inheritance.

src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ private enum IndirectReturnState : byte
6464

6565
private LocalDefinition _returnTemp;
6666

67+
/// <summary>
68+
/// True if there was a <see cref="ILOpCode.Localloc"/> anywhere in the method. This will
69+
/// affect whether or not we require the locals init flag to be marked, since locals init
70+
/// affects <see cref="ILOpCode.Localloc"/>.
71+
/// </summary>
72+
private bool _sawStackalloc;
73+
6774
public CodeGenerator(
6875
MethodSymbol method,
6976
BoundStatement boundBody,
@@ -188,18 +195,24 @@ internal static bool IsStackLocal(LocalSymbol local, HashSet<LocalSymbol> stackL
188195

189196
private bool IsStackLocal(LocalSymbol local) => IsStackLocal(local, _stackLocals);
190197

191-
public void Generate()
198+
public void Generate(out bool hasStackalloc)
192199
{
193200
this.GenerateImpl();
201+
hasStackalloc = _sawStackalloc;
194202

195203
Debug.Assert(_asyncCatchHandlerOffset < 0);
196204
Debug.Assert(_asyncYieldPoints == null);
197205
Debug.Assert(_asyncResumePoints == null);
198206
}
199207

200-
public void Generate(out int asyncCatchHandlerOffset, out ImmutableArray<int> asyncYieldPoints, out ImmutableArray<int> asyncResumePoints)
208+
public void Generate(
209+
out int asyncCatchHandlerOffset,
210+
out ImmutableArray<int> asyncYieldPoints,
211+
out ImmutableArray<int> asyncResumePoints,
212+
out bool hasStackAlloc)
201213
{
202214
this.GenerateImpl();
215+
hasStackAlloc = _sawStackalloc;
203216
Debug.Assert(_asyncCatchHandlerOffset >= 0);
204217

205218
asyncCatchHandlerOffset = _builder.GetILOffsetFromMarker(_asyncCatchHandlerOffset);

src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,6 +1916,7 @@ private void EmitConvertedStackAllocExpression(BoundConvertedStackAllocExpressio
19161916
// we can ignore that if the actual result is unused
19171917
if (used)
19181918
{
1919+
_sawStackalloc = true;
19191920
_builder.EmitOpCode(ILOpCode.Localloc);
19201921
}
19211922

src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@ private void CompileMethod(
892892
_cancellationToken.ThrowIfCancellationRequested();
893893
SourceMemberMethodSymbol sourceMethod = methodSymbol as SourceMemberMethodSymbol;
894894

895-
if (methodSymbol.IsAbstract)
895+
if (methodSymbol.IsAbstract || methodSymbol.ContainingType?.IsDelegateType() == true)
896896
{
897897
if ((object)sourceMethod != null)
898898
{
@@ -1412,7 +1412,8 @@ private static MethodBody GenerateMethodBody(
14121412
var localSlotManager = new LocalSlotManager(variableSlotAllocatorOpt);
14131413
var optimizations = compilation.Options.OptimizationLevel;
14141414

1415-
ILBuilder builder = new ILBuilder(moduleBuilder, localSlotManager, optimizations);
1415+
ILBuilder builder = new ILBuilder(moduleBuilder, localSlotManager, optimizations, method.AreLocalsZeroed);
1416+
bool hasStackalloc;
14161417
DiagnosticBag diagnosticsForThisMethod = DiagnosticBag.GetInstance();
14171418
try
14181419
{
@@ -1449,7 +1450,7 @@ private static MethodBody GenerateMethodBody(
14491450

14501451
if (isAsyncStateMachine)
14511452
{
1452-
codeGen.Generate(out int asyncCatchHandlerOffset, out var asyncYieldPoints, out var asyncResumePoints);
1453+
codeGen.Generate(out int asyncCatchHandlerOffset, out var asyncYieldPoints, out var asyncResumePoints, out hasStackalloc);
14531454

14541455
// The exception handler IL offset is used by the debugger to treat exceptions caught by the marked catch block as "user unhandled".
14551456
// This is important for async void because async void exceptions generally result in the process being terminated,
@@ -1470,7 +1471,7 @@ private static MethodBody GenerateMethodBody(
14701471
}
14711472
else
14721473
{
1473-
codeGen.Generate();
1474+
codeGen.Generate(out hasStackalloc);
14741475

14751476
if ((object)kickoffMethod != null)
14761477
{
@@ -1532,6 +1533,8 @@ private static MethodBody GenerateMethodBody(
15321533
builder.RealizedSequencePoints,
15331534
debugDocumentProvider,
15341535
builder.RealizedExceptionHandlers,
1536+
builder.AreLocalsZeroed,
1537+
hasStackalloc,
15351538
builder.GetAllScopes(),
15361539
builder.HasDynamicLocal,
15371540
importScopeOpt,
@@ -1776,8 +1779,10 @@ private static BoundBlock BindMethodBody(MethodSymbol method, TypeCompilationSta
17761779

17771780
private static BoundStatement BindImplicitConstructorInitializerIfAny(MethodSymbol method, TypeCompilationState compilationState, DiagnosticBag diagnostics)
17781781
{
1782+
Debug.Assert(!method.ContainingType.IsDelegateType());
1783+
17791784
// delegates have constructors but not constructor initializers
1780-
if (method.MethodKind == MethodKind.Constructor && !method.ContainingType.IsDelegateType() && !method.IsExtern)
1785+
if (method.MethodKind == MethodKind.Constructor && !method.IsExtern)
17811786
{
17821787
var compilation = method.DeclaringCompilation;
17831788
var initializerInvocation = BindImplicitConstructorInitializer(method, diagnostics, compilation);

src/Compilers/CSharp/Portable/Lowering/LambdaRewriter/SynthesizedClosureEnvironment.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ internal override IEnumerable<FieldSymbol> GetFieldsToEmit()
127127

128128
public override Symbol ContainingSymbol => _topLevelMethod.ContainingSymbol;
129129

130+
// Closures in the same method share the same SynthesizedClosureEnvironment. We must
131+
// always return true because two closures in the same method might have different
132+
// AreLocalsZeroed flags.
133+
public sealed override bool AreLocalsZeroed => true;
134+
130135
// The lambda method contains user code from the lambda
131136
bool ISynthesizedMethodBodyImplementationSymbol.HasMethodBodyDependency => true;
132137

src/Compilers/CSharp/Portable/Lowering/LambdaRewriter/SynthesizedClosureMethod.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
22

3+
#nullable enable
4+
35
using System.Collections.Immutable;
46
using Microsoft.CodeAnalysis.CodeGen;
57
using Microsoft.CodeAnalysis.CSharp.Symbols;
@@ -15,9 +17,9 @@ namespace Microsoft.CodeAnalysis.CSharp
1517
/// </summary>
1618
internal sealed class SynthesizedClosureMethod : SynthesizedMethodBaseSymbol, ISynthesizedMethodBodyImplementationSymbol
1719
{
18-
private readonly MethodSymbol _topLevelMethod;
1920
private readonly ImmutableArray<NamedTypeSymbol> _structEnvironments;
2021

22+
internal MethodSymbol TopLevelMethod { get; }
2123
internal readonly DebugId LambdaId;
2224

2325
internal SynthesizedClosureMethod(
@@ -39,35 +41,34 @@ originalMethod is LocalFunctionSymbol
3941
: MakeName(topLevelMethod.Name, topLevelMethodId, closureKind, lambdaId),
4042
MakeDeclarationModifiers(closureKind, originalMethod))
4143
{
42-
_topLevelMethod = topLevelMethod;
44+
TopLevelMethod = topLevelMethod;
4345
ClosureKind = closureKind;
4446
LambdaId = lambdaId;
4547

4648
TypeMap typeMap;
4749
ImmutableArray<TypeParameterSymbol> typeParameters;
48-
ImmutableArray<TypeParameterSymbol> constructedFromTypeParameters;
4950

5051
var lambdaFrame = ContainingType as SynthesizedClosureEnvironment;
5152
switch (closureKind)
5253
{
5354
case ClosureKind.Singleton: // all type parameters on method (except the top level method's)
5455
case ClosureKind.General: // only lambda's type parameters on method (rest on class)
55-
Debug.Assert((object)lambdaFrame != null);
56+
RoslynDebug.Assert(!(lambdaFrame is null));
5657
typeMap = lambdaFrame.TypeMap.WithConcatAlphaRename(
5758
originalMethod,
5859
this,
5960
out typeParameters,
60-
out constructedFromTypeParameters,
61+
out _,
6162
lambdaFrame.OriginalContainingMethodOpt);
6263
break;
6364
case ClosureKind.ThisOnly: // all type parameters on method
6465
case ClosureKind.Static:
65-
Debug.Assert((object)lambdaFrame == null);
66+
RoslynDebug.Assert(lambdaFrame is null);
6667
typeMap = TypeMap.Empty.WithConcatAlphaRename(
6768
originalMethod,
6869
this,
6970
out typeParameters,
70-
out constructedFromTypeParameters,
71+
out _,
7172
stopAt: null);
7273
break;
7374
default:
@@ -162,16 +163,17 @@ protected override ImmutableArray<TypeSymbol> ExtraSynthesizedRefParameters
162163

163164
internal override bool GenerateDebugInfo => !this.IsAsync;
164165
internal override bool IsExpressionBodied => false;
165-
internal MethodSymbol TopLevelMethod => _topLevelMethod;
166166

167167
internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree localTree)
168168
{
169169
// Syntax offset of a syntax node contained in a lambda body is calculated by the containing top-level method.
170170
// The offset is thus relative to the top-level method body start.
171-
return _topLevelMethod.CalculateLocalSyntaxOffset(localPosition, localTree);
171+
return TopLevelMethod.CalculateLocalSyntaxOffset(localPosition, localTree);
172172
}
173173

174-
IMethodSymbolInternal ISynthesizedMethodBodyImplementationSymbol.Method => _topLevelMethod;
174+
public sealed override bool AreLocalsZeroed => TopLevelMethod.AreLocalsZeroed;
175+
176+
IMethodSymbolInternal? ISynthesizedMethodBodyImplementationSymbol.Method => TopLevelMethod;
175177

176178
// The lambda method body needs to be updated when the containing top-level method body is updated.
177179
bool ISynthesizedMethodBodyImplementationSymbol.HasMethodBodyDependency => true;

src/Compilers/CSharp/Portable/Lowering/LocalRewriter/DynamicSiteContainer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.CodeAnalysis.CSharp.Symbols;
55
using System.Diagnostics;
66
using Microsoft.CodeAnalysis.Symbols;
7+
using Roslyn.Utilities;
78

89
namespace Microsoft.CodeAnalysis.CSharp
910
{
@@ -28,6 +29,11 @@ public override TypeKind TypeKind
2829
get { return TypeKind.Class; }
2930
}
3031

32+
public sealed override bool AreLocalsZeroed
33+
{
34+
get { throw ExceptionUtilities.Unreachable; }
35+
}
36+
3137
bool ISynthesizedMethodBodyImplementationSymbol.HasMethodBodyDependency
3238
{
3339
get { return true; }

src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/StateMachineTypeSymbol.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public sealed override ImmutableArray<CSharpAttributeData> GetAttributes()
9494
return _attributes;
9595
}
9696

97+
public sealed override bool AreLocalsZeroed => KickoffMethod.AreLocalsZeroed;
98+
9799
internal override bool HasCodeAnalysisEmbeddedAttribute => false;
98100
}
99101
}

src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/PublicSymbols/AnonymousType.TypePublicSymbol.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,11 @@ public override bool IsSerializable
282282
get { return false; }
283283
}
284284

285+
public sealed override bool AreLocalsZeroed
286+
{
287+
get { throw ExceptionUtilities.Unreachable; }
288+
}
289+
285290
internal override bool HasDeclarativeSecurity
286291
{
287292
get { return false; }

0 commit comments

Comments
 (0)