Skip to content

Commit 511526c

Browse files
committed
Collection expression spread optimization
1 parent 132e539 commit 511526c

File tree

4 files changed

+1489
-569
lines changed

4 files changed

+1489
-569
lines changed

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

Lines changed: 241 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Collections.Immutable;
77
using System.Diagnostics;
8+
using System.Diagnostics.CodeAnalysis;
89
using System.Linq;
910
using Microsoft.CodeAnalysis.CSharp.CodeGen;
1011
using Microsoft.CodeAnalysis.CSharp.Symbols;
@@ -443,6 +444,44 @@ private BoundExpression CreateAndPopulateArray(BoundCollectionExpression node, A
443444
throw ExceptionUtilities.UnexpectedValue(node);
444445
}
445446

447+
// Collection-expr is of the form `[..spreadExpression]`, where 'spreadExpression' has same element type as the target collection.
448+
// Optimize to `spreadExpression.ToArray()` if possible.
449+
if (node is { Elements: [BoundCollectionExpressionSpreadElement { Expression: { } spreadExpression } spreadElement] }
450+
&& spreadElement.IteratorBody is BoundExpressionStatement expressionStatement
451+
&& expressionStatement.Expression is not BoundConversion)
452+
{
453+
var spreadTypeOriginalDefinition = spreadExpression.Type!.OriginalDefinition;
454+
if (tryGetToArrayMethod(spreadTypeOriginalDefinition, WellKnownType.System_Collections_Generic_List_T, WellKnownMember.System_Collections_Generic_List_T__ToArray) is { } listToArrayMethod)
455+
{
456+
var rewrittenSpreadExpression = VisitExpression(spreadExpression);
457+
return Factory.Call(rewrittenSpreadExpression, listToArrayMethod.AsMember((NamedTypeSymbol)spreadExpression.Type!));
458+
}
459+
460+
if (GetAsSpanMethod(spreadExpression.Type) is { } asSpanMethod)
461+
{
462+
var spanType = (asSpanMethod is null ? spreadExpression : CallAsSpanMethod(spreadExpression, asSpanMethod)).Type!.OriginalDefinition;
463+
if ((tryGetToArrayMethod(spanType, WellKnownType.System_Span_T, WellKnownMember.System_Span_T__ToArray)
464+
?? tryGetToArrayMethod(spanType, WellKnownType.System_ReadOnlySpan_T, WellKnownMember.System_ReadOnlySpan_T__ToArray))
465+
is { } toArrayMethod)
466+
{
467+
var rewrittenSpreadExpression = VisitExpression(spreadExpression);
468+
if (asSpanMethod is not null)
469+
rewrittenSpreadExpression = CallAsSpanMethod(rewrittenSpreadExpression, asSpanMethod);
470+
return Factory.Call(rewrittenSpreadExpression, toArrayMethod.AsMember((NamedTypeSymbol)rewrittenSpreadExpression.Type!));
471+
}
472+
}
473+
474+
MethodSymbol? tryGetToArrayMethod(TypeSymbol spreadTypeOriginalDefinition, WellKnownType wellKnownType, WellKnownMember wellKnownMember)
475+
{
476+
if (spreadTypeOriginalDefinition.Equals(this._compilation.GetWellKnownType(wellKnownType)))
477+
{
478+
return _factory.WellKnownMethod(wellKnownMember, isOptional: true);
479+
}
480+
481+
return null;
482+
}
483+
}
484+
446485
if (numberIncludingLastSpread == 0)
447486
{
448487
int knownLength = elements.Length;
@@ -492,6 +531,7 @@ private BoundExpression CreateAndPopulateArray(BoundCollectionExpression node, A
492531
localsBuilder.Add(arrayTemp);
493532
sideEffects.Add(assignmentToTemp);
494533

534+
BoundLocal? targetSpanTemp = null;
495535
AddCollectionExpressionElements(
496536
elements,
497537
arrayTemp,
@@ -522,6 +562,25 @@ private BoundExpression CreateAndPopulateArray(BoundCollectionExpression node, A
522562
_factory.Binary(BinaryOperatorKind.Addition, indexTemp.Type, indexTemp, _factory.Literal(1)),
523563
isRef: false,
524564
indexTemp.Type));
565+
},
566+
(sideEffects, targetArray, spreadElement, rewrittenSpreadOperand) =>
567+
{
568+
if (TryPrepareCopyToOptimization(spreadElement, rewrittenSpreadOperand) is not var (spanSliceMethod, spreadElementAsSpan, getLengthMethod, copyToMethod))
569+
return false;
570+
571+
if (targetSpanTemp is null)
572+
{
573+
var targetSpan = TryConvertToSpanOrReadOnlySpan(targetArray);
574+
if (targetSpan is null)
575+
return false;
576+
577+
targetSpanTemp = _factory.StoreToTemp(targetSpan, out var assignmentToTemp);
578+
localsBuilder.Add(targetSpanTemp);
579+
sideEffects.Add(assignmentToTemp);
580+
}
581+
582+
PerformCopyToOptimization(sideEffects, localsBuilder, indexTemp, targetSpanTemp, rewrittenSpreadOperand, spanSliceMethod, spreadElementAsSpan, getLengthMethod, copyToMethod);
583+
return true;
525584
});
526585

527586
var locals = localsBuilder.SelectAsArray(l => l.LocalSymbol);
@@ -535,6 +594,155 @@ private BoundExpression CreateAndPopulateArray(BoundCollectionExpression node, A
535594
arrayType);
536595
}
537596

597+
private MethodSymbol? GetAsSpanMethod(TypeSymbol type)
598+
{
599+
if (type is NamedTypeSymbol spanType
600+
&& (spanType.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_Span_T), TypeCompareKind.ConsiderEverything)
601+
|| spanType.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.ConsiderEverything)))
602+
{
603+
return null;
604+
}
605+
606+
if (type is ArrayTypeSymbol { IsSZArray: true } arrayType
607+
&& _factory.WellKnownMethod(WellKnownMember.System_Span_T__ctor_Array, isOptional: true) is { } spanCtorArray)
608+
{
609+
return spanCtorArray.AsMember(spanCtorArray.ContainingType.Construct(arrayType.ElementType));
610+
}
611+
612+
if (type is NamedTypeSymbol immutableArrayType
613+
&& immutableArrayType.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_Collections_Immutable_ImmutableArray_T), TypeCompareKind.ConsiderEverything)
614+
&& _factory.WellKnownMethod(WellKnownMember.System_Collections_Immutable_ImmutableArray_T__AsSpan, isOptional: true) is { } immutableArrayAsSpanMethod)
615+
{
616+
return immutableArrayAsSpanMethod!.AsMember(immutableArrayType);
617+
}
618+
619+
if (type is NamedTypeSymbol listType
620+
&& listType.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_Collections_Generic_List_T), TypeCompareKind.ConsiderEverything)
621+
&& _factory.WellKnownMethod(WellKnownMember.System_Runtime_InteropServices_CollectionsMarshal__AsSpan_T, isOptional: true) is { } collectionsMarshalAsSpanMethod)
622+
{
623+
return collectionsMarshalAsSpanMethod.Construct(listType.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type);
624+
}
625+
626+
return null;
627+
}
628+
629+
private BoundExpression? TryConvertToSpanOrReadOnlySpan(BoundExpression expression)
630+
{
631+
var type = expression.Type;
632+
Debug.Assert(type is not null);
633+
634+
if (GetAsSpanMethod(type) is not { } asSpanMethod)
635+
{
636+
return null;
637+
}
638+
639+
if (asSpanMethod is null)
640+
{
641+
return expression;
642+
}
643+
else
644+
{
645+
return CallAsSpanMethod(expression, asSpanMethod);
646+
}
647+
}
648+
649+
private BoundExpression CallAsSpanMethod(BoundExpression spreadExpression, MethodSymbol asSpanMethod)
650+
{
651+
if (asSpanMethod is MethodSymbol { MethodKind: MethodKind.Constructor } constructor)
652+
{
653+
return _factory.New(constructor, spreadExpression);
654+
}
655+
else if (asSpanMethod is MethodSymbol { IsStatic: true, ParameterCount: 1 })
656+
{
657+
return _factory.Call(receiver: null, asSpanMethod, spreadExpression);
658+
}
659+
else
660+
{
661+
return _factory.Call(spreadExpression, asSpanMethod);
662+
}
663+
}
664+
665+
/// <summary>
666+
/// Verifies presence of methods necessary for the CopyTo optimization
667+
/// without performing mutating actions e.g. appending to side effects or locals builders.
668+
/// </summary>
669+
private (MethodSymbol spanSliceMethod, BoundExpression spreadElementAsSpan, MethodSymbol getLengthMethod, MethodSymbol copyToMethod)? TryPrepareCopyToOptimization(
670+
BoundCollectionExpressionSpreadElement spreadElement,
671+
BoundExpression rewrittenSpreadOperand)
672+
{
673+
// Cannot use CopyTo when spread element has non-identity conversion to target element type.
674+
// Could do a covariant conversion of ReadOnlySpan in future: https://github.com/dotnet/roslyn/issues/71106
675+
if (spreadElement.IteratorBody is not BoundExpressionStatement expressionStatement || expressionStatement.Expression is BoundConversion)
676+
return null;
677+
678+
if (_factory.WellKnownMethod(WellKnownMember.System_Span_T__Slice_Int_Int, isOptional: true) is not { } spanSliceMethod)
679+
return null;
680+
681+
if (TryConvertToSpanOrReadOnlySpan(rewrittenSpreadOperand) is not { } spreadOperandAsSpan)
682+
return null;
683+
684+
if ((tryGetSpanMethodsForSpread(WellKnownType.System_ReadOnlySpan_T, WellKnownMember.System_ReadOnlySpan_T__get_Length, WellKnownMember.System_ReadOnlySpan_T__CopyTo_Span_T)
685+
?? tryGetSpanMethodsForSpread(WellKnownType.System_Span_T, WellKnownMember.System_Span_T__get_Length, WellKnownMember.System_Span_T__CopyTo_Span_T))
686+
is not (var getLengthMethod, var copyToMethod))
687+
{
688+
return null;
689+
}
690+
691+
return (spanSliceMethod, spreadOperandAsSpan, getLengthMethod, copyToMethod);
692+
693+
// gets either Span or ReadOnlySpan methods for operating on the source spread element.
694+
(MethodSymbol getLengthMethod, MethodSymbol copyToMethod)? tryGetSpanMethodsForSpread(
695+
WellKnownType wellKnownSpanType,
696+
WellKnownMember getLengthMember,
697+
WellKnownMember copyToMember)
698+
{
699+
if (spreadOperandAsSpan.Type!.OriginalDefinition.Equals(this._compilation.GetWellKnownType(wellKnownSpanType))
700+
&& _factory.WellKnownMethod(getLengthMember, isOptional: true) is { } getLengthMethod
701+
&& _factory.WellKnownMethod(copyToMember, isOptional: true) is { } copyToMethod)
702+
{
703+
return (getLengthMethod!, copyToMethod!);
704+
}
705+
706+
return null;
707+
}
708+
}
709+
710+
private void PerformCopyToOptimization(
711+
ArrayBuilder<BoundExpression> sideEffects,
712+
ArrayBuilder<BoundLocal> localsBuilder,
713+
BoundLocal indexTemp,
714+
BoundExpression spanTemp,
715+
BoundExpression rewrittenSpreadOperand,
716+
MethodSymbol spanSliceMethod,
717+
BoundExpression spreadOperandAsSpan,
718+
MethodSymbol getLengthMethod,
719+
MethodSymbol copyToMethod)
720+
{
721+
// before:
722+
// ..e1 // in [e0, ..e1]
723+
//
724+
// after (roughly):
725+
// var e1Span = e1.AsSpan();
726+
// e1Span.CopyTo(destinationSpan.Slice(indexTemp, e1Span.Length);
727+
// indexTemp += e1Span.Length;
728+
729+
Debug.Assert((object)spreadOperandAsSpan != rewrittenSpreadOperand || spreadOperandAsSpan is BoundLocal { LocalSymbol.SynthesizedKind: SynthesizedLocalKind.LoweringTemp });
730+
if ((object)spreadOperandAsSpan != rewrittenSpreadOperand)
731+
{
732+
spreadOperandAsSpan = _factory.StoreToTemp(spreadOperandAsSpan, out var assignmentToTemp);
733+
sideEffects.Add(assignmentToTemp);
734+
localsBuilder.Add((BoundLocal)spreadOperandAsSpan);
735+
}
736+
737+
// e1Span.CopyTo(destinationSpan.Slice(indexTemp, e1Span.Length);
738+
var spreadLength = _factory.Call(spreadOperandAsSpan, getLengthMethod.AsMember((NamedTypeSymbol)spreadOperandAsSpan.Type!));
739+
var targetSlice = _factory.Call(spanTemp, spanSliceMethod.AsMember((NamedTypeSymbol)spanTemp.Type!), indexTemp, spreadLength);
740+
sideEffects.Add(_factory.Call(spreadOperandAsSpan, copyToMethod.AsMember((NamedTypeSymbol)spreadOperandAsSpan.Type!), targetSlice));
741+
742+
// indexTemp += e1Span.Length;
743+
sideEffects.Add(new BoundAssignmentOperator(rewrittenSpreadOperand.Syntax, indexTemp, _factory.Binary(BinaryOperatorKind.Addition, indexTemp.Type, indexTemp, spreadLength), isRef: false, indexTemp.Type));
744+
}
745+
538746
/// <summary>
539747
/// Create and populate an list from a collection expression.
540748
/// The collection may or may not have a known length.
@@ -648,11 +856,20 @@ private BoundExpression CreateAndPopulateList(BoundCollectionExpression node, Ty
648856
_factory.Binary(BinaryOperatorKind.Addition, indexTemp.Type, indexTemp, _factory.Literal(1)),
649857
isRef: false,
650858
indexTemp.Type));
859+
},
860+
(sideEffects, spanTemp, spreadElement, rewrittenSpreadOperand) =>
861+
{
862+
if (TryPrepareCopyToOptimization(spreadElement, rewrittenSpreadOperand) is not var (spanSliceMethod, spreadElementAsSpan, getLengthMethod, copyToMethod))
863+
return false;
864+
865+
PerformCopyToOptimization(sideEffects, localsBuilder, indexTemp, spanTemp, rewrittenSpreadOperand, spanSliceMethod, spreadElementAsSpan, getLengthMethod, copyToMethod);
866+
return true;
651867
});
652868
}
653869
else
654870
{
655-
var addMethod = ((MethodSymbol)_factory.WellKnownMember(WellKnownMember.System_Collections_Generic_List_T__Add)).AsMember(collectionType);
871+
var addMethod = _factory.WellKnownMethod(WellKnownMember.System_Collections_Generic_List_T__Add).AsMember(collectionType);
872+
var addRangeMethod = _factory.WellKnownMethod(WellKnownMember.System_Collections_Generic_List_T__AddRange, isOptional: true)?.AsMember(collectionType);
656873
AddCollectionExpressionElements(
657874
elements,
658875
listTemp,
@@ -664,6 +881,24 @@ private BoundExpression CreateAndPopulateList(BoundCollectionExpression node, Ty
664881
// list.Add(element);
665882
expressions.Add(
666883
_factory.Call(listTemp, addMethod, rewrittenValue));
884+
},
885+
(sideEffects, spanTemp, spreadElement, rewrittenSpreadOperand) =>
886+
{
887+
if (addRangeMethod is null)
888+
return false;
889+
890+
var type = rewrittenSpreadOperand.Type!;
891+
892+
var useSiteInfo = GetNewCompoundUseSiteInfo();
893+
var conversion = _compilation.Conversions.ClassifyConversionFromType(type, addRangeMethod.Parameters[0].Type, isChecked: false, ref useSiteInfo);
894+
_diagnostics.Add(rewrittenSpreadOperand.Syntax, useSiteInfo);
895+
if (conversion.IsImplicit && conversion.IsReference)
896+
{
897+
sideEffects.Add(_factory.Call(listTemp, addRangeMethod, MakeConversionNode(rewrittenSpreadOperand, type, @checked: false, markAsChecked: true)));
898+
return true;
899+
}
900+
901+
return false;
667902
});
668903
}
669904

@@ -708,7 +943,8 @@ private void AddCollectionExpressionElements(
708943
ArrayBuilder<BoundLocal> rewrittenExpressions,
709944
int numberIncludingLastSpread,
710945
ArrayBuilder<BoundExpression> sideEffects,
711-
Action<ArrayBuilder<BoundExpression>, BoundExpression, BoundExpression> addElement)
946+
Action<ArrayBuilder<BoundExpression>, BoundExpression, BoundExpression> addElement,
947+
Func<ArrayBuilder<BoundExpression>, BoundExpression, BoundCollectionExpressionSpreadElement, BoundExpression, bool>? tryOptimizeSpreadElement = null)
712948
{
713949
for (int i = 0; i < elements.Length; i++)
714950
{
@@ -719,6 +955,9 @@ private void AddCollectionExpressionElements(
719955

720956
if (element is BoundCollectionExpressionSpreadElement spreadElement)
721957
{
958+
if (tryOptimizeSpreadElement?.Invoke(sideEffects, rewrittenReceiver, spreadElement, rewrittenExpression) == true)
959+
continue;
960+
722961
var rewrittenElement = MakeCollectionExpressionSpreadElement(
723962
spreadElement,
724963
rewrittenExpression,

0 commit comments

Comments
 (0)