Skip to content

Commit c6a0795

Browse files
authored
Optimize single spread collection expression for List<T> (#74769)
1 parent 935befe commit c6a0795

File tree

4 files changed

+995
-605
lines changed

4 files changed

+995
-605
lines changed

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

Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,17 @@ private BoundExpression RewriteCollectionExpressionConversion(Conversion convers
4343
switch (collectionTypeKind)
4444
{
4545
case CollectionExpressionTypeKind.ImplementsIEnumerable:
46-
if (useListOptimization(_compilation, node, out var listElementType))
46+
if (ConversionsBase.IsSpanOrListType(_compilation, node.Type, WellKnownType.System_Collections_Generic_List_T, out var listElementType))
4747
{
48-
return CreateAndPopulateList(node, listElementType, node.Elements.SelectAsArray(static (element, node) => unwrapListElement(node, element), node));
48+
if (TryRewriteSingleElementSpreadToList(node, listElementType, out var result))
49+
{
50+
return result;
51+
}
52+
53+
if (useListOptimization(_compilation, node))
54+
{
55+
return CreateAndPopulateList(node, listElementType, node.Elements.SelectAsArray(static (element, node) => unwrapListElement(node, element), node));
56+
}
4957
}
5058
return VisitCollectionInitializerCollectionExpression(node, node.Type);
5159
case CollectionExpressionTypeKind.Array:
@@ -88,12 +96,8 @@ private BoundExpression RewriteCollectionExpressionConversion(Conversion convers
8896

8997
// If the collection type is List<T> and items are added using the expected List<T>.Add(T) method,
9098
// then construction can be optimized to use CollectionsMarshal methods.
91-
static bool useListOptimization(CSharpCompilation compilation, BoundCollectionExpression node, out TypeWithAnnotations elementType)
99+
static bool useListOptimization(CSharpCompilation compilation, BoundCollectionExpression node)
92100
{
93-
if (!ConversionsBase.IsSpanOrListType(compilation, node.Type, WellKnownType.System_Collections_Generic_List_T, out elementType))
94-
{
95-
return false;
96-
}
97101
var elements = node.Elements;
98102
if (elements.Length == 0)
99103
{
@@ -151,6 +155,62 @@ static BoundNode unwrapListElement(BoundCollectionExpression node, BoundNode ele
151155
}
152156
}
153157

158+
// If we have something like `List<int> l = [.. someEnumerable]`
159+
// try rewrite it using `Enumerable.ToList` member if possible
160+
private bool TryRewriteSingleElementSpreadToList(BoundCollectionExpression node, TypeWithAnnotations listElementType, [NotNullWhen(true)] out BoundExpression? result)
161+
{
162+
result = null;
163+
164+
if (node.Elements is not [BoundCollectionExpressionSpreadElement singleSpread])
165+
{
166+
return false;
167+
}
168+
169+
if (!TryGetWellKnownTypeMember(node.Syntax, WellKnownMember.System_Linq_Enumerable__ToList, out MethodSymbol? toListGeneric, isOptional: true))
170+
{
171+
return false;
172+
}
173+
174+
var toListOfElementType = toListGeneric.Construct([listElementType]);
175+
176+
Debug.Assert(singleSpread.Expression.Type is not null);
177+
178+
if (!ShouldUseAddRangeOrToListMethod(singleSpread.Expression.Type, toListOfElementType.Parameters[0].Type, singleSpread.EnumeratorInfoOpt?.GetEnumeratorInfo.Method))
179+
{
180+
return false;
181+
}
182+
183+
var rewrittenSpreadExpression = VisitExpression(singleSpread.Expression);
184+
result = _factory.Call(receiver: null, toListOfElementType, rewrittenSpreadExpression);
185+
return true;
186+
}
187+
188+
private bool ShouldUseAddRangeOrToListMethod(TypeSymbol spreadType, TypeSymbol targetEnumerableType, MethodSymbol? getEnumeratorMethod)
189+
{
190+
Debug.Assert(targetEnumerableType.OriginalDefinition == (object)_compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T));
191+
192+
var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;
193+
194+
Conversion conversion;
195+
196+
// If collection has a struct enumerator but doesn't implement ICollection<T>
197+
// then manual `foreach` is always more efficient then using `ToList` or `AddRange` methods
198+
if (getEnumeratorMethod?.ReturnType.IsValueType == true)
199+
{
200+
var iCollectionOfTType = _compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T);
201+
var iCollectionOfElementType = iCollectionOfTType.Construct(((NamedTypeSymbol)targetEnumerableType).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics);
202+
203+
conversion = _compilation.Conversions.ClassifyBuiltInConversion(spreadType, iCollectionOfElementType, isChecked: false, ref discardedUseSiteInfo);
204+
if (conversion.Kind is not (ConversionKind.Identity or ConversionKind.ImplicitReference))
205+
{
206+
return false;
207+
}
208+
}
209+
210+
conversion = _compilation.Conversions.ClassifyImplicitConversionFromType(spreadType, targetEnumerableType, ref discardedUseSiteInfo);
211+
return conversion.Kind is ConversionKind.Identity or ConversionKind.ImplicitReference;
212+
}
213+
154214
private static bool CanOptimizeSingleSpreadAsCollectionBuilderArgument(BoundCollectionExpression node, [NotNullWhen(true)] out BoundExpression? spreadExpression)
155215
{
156216
spreadExpression = null;
@@ -1064,42 +1124,18 @@ private BoundExpression CreateAndPopulateList(BoundCollectionExpression node, Ty
10641124
},
10651125
tryOptimizeSpreadElement: (ArrayBuilder<BoundExpression> sideEffects, BoundExpression listTemp, BoundCollectionExpressionSpreadElement spreadElement, BoundExpression rewrittenSpreadOperand) =>
10661126
{
1127+
Debug.Assert(rewrittenSpreadOperand.Type is not null);
1128+
10671129
if (addRangeMethod is null)
10681130
return false;
10691131

1070-
Conversion conversion;
1071-
1072-
if (spreadElement.EnumeratorInfoOpt is { } enumeratorInfo)
1132+
if (!ShouldUseAddRangeOrToListMethod(rewrittenSpreadOperand.Type, addRangeMethod.Parameters[0].Type, spreadElement.EnumeratorInfoOpt?.GetEnumeratorInfo.Method))
10731133
{
1074-
var iCollectionOfTType = _compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T);
1075-
var iCollectionOfElementType = iCollectionOfTType.Construct(enumeratorInfo.ElementType);
1076-
var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;
1077-
1078-
// If collection has a struct enumerator but doesn't implement ICollection<T>
1079-
// then manual `foreach` is always more efficient then using `AddRange` method
1080-
if (enumeratorInfo.GetEnumeratorInfo.Method.ReturnType.IsValueType)
1081-
{
1082-
conversion = _compilation.Conversions.ClassifyBuiltInConversion(enumeratorInfo.CollectionType, iCollectionOfElementType, isChecked: false, ref discardedUseSiteInfo);
1083-
if (!(conversion.Kind is ConversionKind.Identity or ConversionKind.ImplicitReference))
1084-
{
1085-
return false;
1086-
}
1087-
}
1088-
}
1089-
1090-
var type = rewrittenSpreadOperand.Type!;
1091-
1092-
var useSiteInfo = GetNewCompoundUseSiteInfo();
1093-
conversion = _compilation.Conversions.ClassifyConversionFromType(type, addRangeMethod.Parameters[0].Type, isChecked: false, ref useSiteInfo);
1094-
_diagnostics.Add(rewrittenSpreadOperand.Syntax, useSiteInfo);
1095-
if (conversion.Kind is ConversionKind.Identity or ConversionKind.ImplicitReference)
1096-
{
1097-
conversion.MarkUnderlyingConversionsCheckedRecursive();
1098-
sideEffects.Add(_factory.Call(listTemp, addRangeMethod, rewrittenSpreadOperand));
1099-
return true;
1134+
return false;
11001135
}
11011136

1102-
return false;
1137+
sideEffects.Add(_factory.Call(listTemp, addRangeMethod, rewrittenSpreadOperand));
1138+
return true;
11031139
});
11041140
}
11051141

0 commit comments

Comments
 (0)