-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reuse previous lambda body binding if possible #43366
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,7 +33,7 @@ internal sealed partial class BoundLocalFunctionStatement : IBoundLambdaOrFuncti | |
BoundBlock IBoundLambdaOrFunction.Body { get => this.Body; } | ||
} | ||
|
||
internal struct InferredLambdaReturnType | ||
internal readonly struct InferredLambdaReturnType | ||
{ | ||
internal readonly int NumExpressions; | ||
internal readonly bool HadExpressionlessReturn; | ||
|
@@ -185,7 +185,9 @@ internal static InferredLambdaReturnType InferReturnType(ArrayBuilder<(BoundRetu | |
|
||
HashSet<DiagnosticInfo> useSiteDiagnostics = null; | ||
var bestType = CalculateReturnType(compilation, conversions, delegateType, types, isAsync, node, ref useSiteDiagnostics); | ||
return new InferredLambdaReturnType(types.Count, hasReturnWithoutArgument, refKind, bestType, useSiteDiagnostics.AsImmutableOrEmpty()); | ||
int numExpressions = types.Count; | ||
types.Free(); | ||
return new InferredLambdaReturnType(numExpressions, hasReturnWithoutArgument, refKind, bestType, useSiteDiagnostics.AsImmutableOrEmpty()); | ||
} | ||
|
||
private static TypeWithAnnotations CalculateReturnType( | ||
|
@@ -467,6 +469,17 @@ internal UnboundLambdaState WithCaching(bool includeCache) | |
public abstract RefKind RefKind(int index); | ||
protected abstract BoundBlock BindLambdaBody(LambdaSymbol lambdaSymbol, Binder lambdaBodyBinder, DiagnosticBag diagnostics); | ||
|
||
/// <summary> | ||
/// Return the bound expression if the lambda is has an expression body and can be reused easily. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Typo? |
||
/// This is an optimization only. Implementations can return null to skip reuse. | ||
/// </summary> | ||
protected abstract BoundExpression GetLambdaExpressionBody(BoundBlock body); | ||
|
||
/// <summary> | ||
/// Produce a bound block for the expression returned from GetLambdaExpressionBody. | ||
/// </summary> | ||
protected abstract BoundBlock CreateBlockFromLambdaExpressionBody(Binder lambdaBodyBinder, BoundExpression expression, DiagnosticBag diagnostics); | ||
|
||
public virtual void GenerateAnonymousFunctionConversionError(DiagnosticBag diagnostics, TypeSymbol targetType) | ||
{ | ||
this.Binder.GenerateAnonymousFunctionConversionError(diagnostics, _unboundLambda.Syntax, _unboundLambda, targetType); | ||
|
@@ -541,20 +554,39 @@ private bool DelegateNeedsReturn(MethodSymbol invokeMethod) | |
|
||
private BoundLambda ReallyBind(NamedTypeSymbol delegateType) | ||
{ | ||
// When binding for real (not for return inference), we cannot reuse a body of a lambda | ||
// previously bound for return type inference because we have not converted the returned | ||
// expression(s) to the return type. | ||
var invokeMethod = DelegateInvokeMethod(delegateType); | ||
var returnType = DelegateReturnTypeWithAnnotations(invokeMethod, out RefKind refKind); | ||
|
||
LambdaSymbol lambdaSymbol = CreateLambdaSymbol( | ||
delegateType, | ||
Binder.ContainingMemberOrLambda, | ||
out MethodSymbol invokeMethod, | ||
out TypeWithAnnotations returnType, | ||
out DiagnosticBag diagnostics, | ||
out ReturnInferenceCacheKey cacheKey); | ||
LambdaSymbol lambdaSymbol; | ||
Binder lambdaBodyBinder; | ||
BoundBlock block; | ||
|
||
var diagnostics = DiagnosticBag.GetInstance(); | ||
var compilation = Binder.Compilation; | ||
var cacheKey = ReturnInferenceCacheKey.Create(delegateType, IsAsync); | ||
|
||
CSharpCompilation compilation = Binder.Compilation; | ||
Binder lambdaBodyBinder = new ExecutableCodeBinder(_unboundLambda.Syntax, lambdaSymbol, ParameterBinder(lambdaSymbol, Binder)); | ||
// When binding for real (not for return inference), there is still a good chance | ||
// we could reuse a body of a lambda previous bound for return type inference. | ||
// For simplicity, reuse is limited to expression-bodied lambdas. In those cases, | ||
// we reuse the bound expression and apply any conversion to the return value | ||
// since the inferred return type was not used when binding for return inference. | ||
if (refKind == CodeAnalysis.RefKind.None && | ||
_returnInferenceCache.TryGetValue(cacheKey, out BoundLambda returnInferenceLambda) && | ||
GetLambdaExpressionBody(returnInferenceLambda.Body) is BoundExpression expression && | ||
(lambdaSymbol = returnInferenceLambda.Symbol).RefKind == refKind && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Should we also check that both are None as |
||
(object)LambdaSymbol.InferenceFailureReturnType != lambdaSymbol.ReturnType && | ||
lambdaSymbol.ReturnTypeWithAnnotations.Equals(returnType, TypeCompareKind.ConsiderEverything)) | ||
{ | ||
lambdaBodyBinder = returnInferenceLambda.Binder; | ||
block = CreateBlockFromLambdaExpressionBody(lambdaBodyBinder, expression, diagnostics); | ||
diagnostics.AddRange(returnInferenceLambda.Diagnostics); | ||
} | ||
else | ||
{ | ||
lambdaSymbol = CreateLambdaSymbol(Binder.ContainingMemberOrLambda, returnType, diagnostics, cacheKey.ParameterTypes, cacheKey.ParameterRefKinds, refKind); | ||
lambdaBodyBinder = new ExecutableCodeBinder(_unboundLambda.Syntax, lambdaSymbol, ParameterBinder(lambdaSymbol, Binder)); | ||
block = BindLambdaBody(lambdaSymbol, lambdaBodyBinder, diagnostics); | ||
} | ||
|
||
if (lambdaSymbol.RefKind == CodeAnalysis.RefKind.RefReadOnly) | ||
{ | ||
|
@@ -583,8 +615,6 @@ private BoundLambda ReallyBind(NamedTypeSymbol delegateType) | |
ParameterHelpers.EnsureNullableAttributeExists(compilation, lambdaSymbol, lambdaParameters, diagnostics, modifyCompilation: false); | ||
// Note: we don't need to warn on annotations used in #nullable disable context for lambdas, as this is handled in binding already | ||
|
||
BoundBlock block = BindLambdaBody(lambdaSymbol, lambdaBodyBinder, diagnostics); | ||
|
||
((ExecutableCodeBinder)lambdaBodyBinder).ValidateIteratorMethods(diagnostics); | ||
ValidateUnsafeParameters(diagnostics, cacheKey.ParameterTypes); | ||
|
||
|
@@ -626,16 +656,6 @@ private BoundLambda ReallyBind(NamedTypeSymbol delegateType) | |
return result; | ||
} | ||
|
||
private LambdaSymbol CreateLambdaSymbol(NamedTypeSymbol delegateType, Symbol containingSymbol, out MethodSymbol invokeMethod, out TypeWithAnnotations returnType, out DiagnosticBag diagnostics, out ReturnInferenceCacheKey cacheKey) | ||
{ | ||
invokeMethod = DelegateInvokeMethod(delegateType); | ||
returnType = DelegateReturnTypeWithAnnotations(invokeMethod, out RefKind refKind); | ||
diagnostics = DiagnosticBag.GetInstance(); | ||
cacheKey = ReturnInferenceCacheKey.Create(delegateType, IsAsync); | ||
|
||
return CreateLambdaSymbol(containingSymbol, returnType, diagnostics, cacheKey.ParameterTypes, cacheKey.ParameterRefKinds, refKind); | ||
} | ||
|
||
internal LambdaSymbol CreateLambdaSymbol( | ||
Symbol containingSymbol, | ||
TypeWithAnnotations returnType, | ||
|
@@ -655,7 +675,10 @@ internal LambdaSymbol CreateLambdaSymbol( | |
|
||
internal LambdaSymbol CreateLambdaSymbol(NamedTypeSymbol delegateType, Symbol containingSymbol) | ||
{ | ||
return CreateLambdaSymbol(delegateType, containingSymbol, out _, out _, out _, out _); | ||
var invokeMethod = DelegateInvokeMethod(delegateType); | ||
var returnType = DelegateReturnTypeWithAnnotations(invokeMethod, out RefKind refKind); | ||
var cacheKey = ReturnInferenceCacheKey.Create(delegateType, IsAsync); | ||
return CreateLambdaSymbol(containingSymbol, returnType, new DiagnosticBag(), cacheKey.ParameterTypes, cacheKey.ParameterRefKinds, refKind); | ||
} | ||
|
||
private void ValidateUnsafeParameters(DiagnosticBag diagnostics, ImmutableArray<TypeWithAnnotations> targetParameterTypes) | ||
|
@@ -741,6 +764,7 @@ private BoundLambda ReallyInferReturnType( | |
var block = BindLambdaBody(lambdaSymbol, lambdaBodyBinder, diagnostics); | ||
return (lambdaSymbol, block, lambdaBodyBinder, diagnostics); | ||
} | ||
|
||
public BoundLambda BindForReturnTypeInference(NamedTypeSymbol delegateType) | ||
{ | ||
var cacheKey = ReturnInferenceCacheKey.Create(delegateType, IsAsync); | ||
|
@@ -1216,6 +1240,26 @@ protected override UnboundLambdaState WithCachingCore(bool includeCache) | |
return new PlainUnboundLambdaState(unboundLambda: null, Binder, _parameterNames, _parameterIsDiscardOpt, _parameterTypesWithAnnotations, _parameterRefKinds, _isAsync, includeCache); | ||
} | ||
|
||
protected override BoundExpression GetLambdaExpressionBody(BoundBlock body) | ||
{ | ||
if (IsExpressionLambda) | ||
{ | ||
var statements = body.Statements; | ||
if (statements.Length == 1 && | ||
// To simplify Binder.CreateBlockFromExpression (used below), we only reuse by-value return values. | ||
statements[0] is BoundReturnStatement { RefKind: Microsoft.CodeAnalysis.RefKind.None, ExpressionOpt: BoundExpression expr }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is not obvious why this condition is significant, consider adding a clarifying comment. #Closed |
||
{ | ||
return expr; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is not clear why it is reusable? The name doesn't imply any restrictions on the input. I can imagine a situation when some conversions are have already been applied based on some context, therefore making the expression un-reusable. If the method is not responsible for enforcing reusability, then it should not be part of the name. This is just a helper that extracts return expression from a block with a single ref-none return. #Closed |
||
} | ||
} | ||
return null; | ||
} | ||
|
||
protected override BoundBlock CreateBlockFromLambdaExpressionBody(Binder lambdaBodyBinder, BoundExpression expression, DiagnosticBag diagnostics) | ||
{ | ||
return lambdaBodyBinder.CreateBlockFromExpression((ExpressionSyntax)this.Body, expression, diagnostics); | ||
} | ||
|
||
protected override BoundBlock BindLambdaBody(LambdaSymbol lambdaSymbol, Binder lambdaBodyBinder, DiagnosticBag diagnostics) | ||
{ | ||
if (this.IsExpressionLambda) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -285,7 +285,7 @@ static class Ext | |
|
||
[ConditionalFactAttribute(typeof(IsRelease))] | ||
[WorkItem(40495, "https://github.com/dotnet/roslyn/issues/40495")] | ||
public void NestedLambdas() | ||
public void NestedLambdas_01() | ||
{ | ||
var source = | ||
@"#nullable enable | ||
|
@@ -302,6 +302,45 @@ static void Main() | |
Enumerable.Range(0, 1).Sum(f => | ||
Enumerable.Range(0, 1).Count(g => true))))))); | ||
} | ||
}"; | ||
var comp = CreateCompilation(source); | ||
comp.VerifyDiagnostics(); | ||
} | ||
|
||
// Test should complete in several seconds if UnboundLambda.ReallyBind | ||
// uses results from _returnInferenceCache. | ||
[ConditionalFactAttribute(typeof(IsRelease))] | ||
[WorkItem(1083969, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1083969")] | ||
public void NestedLambdas_02() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What exactly are we testing here? Were we unable to compile the code before? #Closed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're testing performance. On my machine, this test completes in a couple of seconds with this change and 15+ mins without. In reply to: 410978595 [](ancestors = 410978595) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Consider adding a comment to make it easy to analyze failures. In reply to: 411004957 [](ancestors = 411004957,410978595) |
||
{ | ||
var source = | ||
@"using System.Collections.Generic; | ||
using System.Linq; | ||
class Program | ||
{ | ||
static void F(IEnumerable<int[]> x) | ||
{ | ||
x.GroupBy(y => y[1]).SelectMany(x => | ||
x.GroupBy(y => y[2]).SelectMany(x => | ||
x.GroupBy(y => y[3]).SelectMany(x => | ||
x.GroupBy(y => y[4]).SelectMany(x => | ||
x.GroupBy(y => y[5]).SelectMany(x => | ||
x.GroupBy(y => y[6]).SelectMany(x => | ||
x.GroupBy(y => y[7]).SelectMany(x => | ||
x.GroupBy(y => y[8]).SelectMany(x => | ||
x.GroupBy(y => y[9]).SelectMany(x => | ||
x.GroupBy(y => y[0]).SelectMany(x => | ||
x.GroupBy(y => y[1]).SelectMany(x => | ||
x.GroupBy(y => y[2]).SelectMany(x => | ||
x.GroupBy(y => y[3]).SelectMany(x => | ||
x.GroupBy(y => y[4]).SelectMany(x => | ||
x.GroupBy(y => y[5]).SelectMany(x => | ||
x.GroupBy(y => y[6]).SelectMany(x => | ||
x.GroupBy(y => y[7]).SelectMany(x => | ||
x.GroupBy(y => y[8]).SelectMany(x => | ||
x.GroupBy(y => y[9]).SelectMany(x => | ||
x.GroupBy(y => y[0]).Select(x => x.Average(z => z[0]))))))))))))))))))))); | ||
} | ||
}"; | ||
var comp = CreateCompilation(source); | ||
comp.VerifyDiagnostics(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were we leaking this builder before? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
In reply to: 410970357 [](ancestors = 410970357)