-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Optimize single spread collection expression for List<T>
#74769
Changes from 12 commits
b3570d7
a973cc8
ed9f0c6
bc71192
c7a3f51
dfa95e5
6895189
0a66f09
7e5b188
bd468b4
a6e8878
05ffe61
95274af
c53a218
ae1c2be
2c8ac16
4125d52
23d22b2
2234846
4c710c1
e817f62
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 |
---|---|---|
|
@@ -43,9 +43,17 @@ private BoundExpression RewriteCollectionExpressionConversion(Conversion convers | |
switch (collectionTypeKind) | ||
{ | ||
case CollectionExpressionTypeKind.ImplementsIEnumerable: | ||
if (useListOptimization(_compilation, node, out var listElementType)) | ||
if (ConversionsBase.IsSpanOrListType(_compilation, node.Type, WellKnownType.System_Collections_Generic_List_T, out var listElementType)) | ||
{ | ||
return CreateAndPopulateList(node, listElementType, node.Elements.SelectAsArray(static (element, node) => unwrapListElement(node, element), node)); | ||
if (TryRewriteSingleElementSpreadToList(node, listElementType, out var result)) | ||
{ | ||
return result; | ||
} | ||
|
||
if (useListOptimization(_compilation, node)) | ||
{ | ||
return CreateAndPopulateList(node, listElementType, node.Elements.SelectAsArray(static (element, node) => unwrapListElement(node, element), node)); | ||
} | ||
} | ||
return VisitCollectionInitializerCollectionExpression(node, node.Type); | ||
case CollectionExpressionTypeKind.Array: | ||
|
@@ -88,12 +96,8 @@ private BoundExpression RewriteCollectionExpressionConversion(Conversion convers | |
|
||
// If the collection type is List<T> and items are added using the expected List<T>.Add(T) method, | ||
// then construction can be optimized to use CollectionsMarshal methods. | ||
static bool useListOptimization(CSharpCompilation compilation, BoundCollectionExpression node, out TypeWithAnnotations elementType) | ||
static bool useListOptimization(CSharpCompilation compilation, BoundCollectionExpression node) | ||
{ | ||
if (!ConversionsBase.IsSpanOrListType(compilation, node.Type, WellKnownType.System_Collections_Generic_List_T, out elementType)) | ||
{ | ||
return false; | ||
} | ||
var elements = node.Elements; | ||
if (elements.Length == 0) | ||
{ | ||
|
@@ -151,6 +155,59 @@ static BoundNode unwrapListElement(BoundCollectionExpression node, BoundNode ele | |
} | ||
} | ||
|
||
// If we have something like `List<int> l = [.. someEnumerable]` | ||
// try rewrite it using `Enumerable.ToList` member if possible | ||
private bool TryRewriteSingleElementSpreadToList(BoundCollectionExpression node, TypeWithAnnotations listElementType, [NotNullWhen(true)] out BoundExpression? result) | ||
{ | ||
result = null; | ||
|
||
if (node.Elements is not [BoundCollectionExpressionSpreadElement singleSpread]) | ||
{ | ||
return false; | ||
} | ||
|
||
if (!ShouldUseAddRangeOrToListMethod(singleSpread.Expression.Type!, singleSpread.EnumeratorInfoOpt?.GetEnumeratorInfo.Method, listElementType)) | ||
DoctorKrolic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return false; | ||
} | ||
|
||
if (TryGetWellKnownTypeMember(node.Syntax, WellKnownMember.System_Linq_Enumerable__ToList, out MethodSymbol? toListGeneric, isOptional: true)) | ||
{ | ||
var toListOfElementType = toListGeneric.Construct([listElementType]); | ||
result = _factory.Call(receiver: null, toListOfElementType, singleSpread.Expression); | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private bool ShouldUseAddRangeOrToListMethod(TypeSymbol spreadType, MethodSymbol? getEnumeratorMethod, TypeWithAnnotations elementType) | ||
{ | ||
var iEnumerableOfTType = _compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T); | ||
var iEnumerableOfElementType = iEnumerableOfTType.Construct([elementType]); | ||
|
||
var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded; | ||
|
||
var conversion = _compilation.Conversions.ClassifyImplicitConversionFromType(spreadType, iEnumerableOfElementType, ref discardedUseSiteInfo); | ||
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 maybe use the actual use site info for thisand report it if we take the optimzal code path an the end? Unline the 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. I guess the original code for the AddRange case was reporting the use-site diagnostic, but I think it's fine to drop it. The code we generate does not actually use this type in any way, it's just a hint for us to decide if AddRange/ToList is likely to be efficient. 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.
Let's use the code from 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. Also, the 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.
I didn't implement logic in the binding layer, that fills in |
||
if (conversion.Kind is not (ConversionKind.Identity or ConversionKind.ImplicitReference)) | ||
{ | ||
return false; | ||
} | ||
|
||
if (getEnumeratorMethod?.ReturnType.IsValueType == true) | ||
{ | ||
var iCollectionOfTType = _compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T); | ||
var iCollectionOfElementType = iCollectionOfTType.Construct([elementType]); | ||
|
||
if (!spreadType.ImplementsInterface(iCollectionOfElementType, ref discardedUseSiteInfo)) | ||
DoctorKrolic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
DoctorKrolic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
private static bool CanOptimizeSingleSpreadAsCollectionBuilderArgument(BoundCollectionExpression node, [NotNullWhen(true)] out BoundExpression? spreadExpression) | ||
{ | ||
spreadExpression = null; | ||
|
@@ -1067,34 +1124,13 @@ private BoundExpression CreateAndPopulateList(BoundCollectionExpression node, Ty | |
if (addRangeMethod is null) | ||
return false; | ||
|
||
if (spreadElement.EnumeratorInfoOpt is { } enumeratorInfo) | ||
{ | ||
var iCollectionOfTType = _compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T); | ||
var iCollectionOfElementType = iCollectionOfTType.Construct(enumeratorInfo.ElementType); | ||
var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded; | ||
|
||
// If collection has a struct enumerator but doesn't implement ICollection<T> | ||
// then manual `foreach` is always more efficient then using `AddRange` method | ||
if (enumeratorInfo.GetEnumeratorInfo.Method.ReturnType.IsValueType && | ||
!enumeratorInfo.CollectionType.ImplementsInterface(iCollectionOfElementType, ref discardedUseSiteInfo)) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
var type = rewrittenSpreadOperand.Type!; | ||
|
||
var useSiteInfo = GetNewCompoundUseSiteInfo(); | ||
var conversion = _compilation.Conversions.ClassifyConversionFromType(type, addRangeMethod.Parameters[0].Type, isChecked: false, ref useSiteInfo); | ||
_diagnostics.Add(rewrittenSpreadOperand.Syntax, useSiteInfo); | ||
if (conversion.IsIdentity || (conversion.IsImplicit && conversion.IsReference)) | ||
if (!ShouldUseAddRangeOrToListMethod(rewrittenSpreadOperand.Type!, spreadElement.EnumeratorInfoOpt?.GetEnumeratorInfo.Method, elementType)) | ||
DoctorKrolic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
conversion.MarkUnderlyingConversionsCheckedRecursive(); | ||
sideEffects.Add(_factory.Call(listTemp, addRangeMethod, rewrittenSpreadOperand)); | ||
return true; | ||
return false; | ||
} | ||
|
||
return false; | ||
sideEffects.Add(_factory.Call(listTemp, addRangeMethod, rewrittenSpreadOperand)); | ||
return true; | ||
}); | ||
} | ||
|
||
|
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.
I wish collection expressions were implemented using a chain of nodes based on how the codegen would work, then this kind of optimization would be a lot simpler to implement IMO, for example
[a, b, .. array, .. list, etc]
yields[ Block(a, b), SpreadArray(array), SpreadList(list), Element(etc) ]
and then pattern-match on that, this would separate the initial analysis and the final codegen. The structure would look a lot like how string concat works but a little more involved to aid codegen.