Skip to content
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

Collection expression spread optimization #71195

Merged

Conversation

RikkiGibson
Copy link
Contributor

@RikkiGibson RikkiGibson commented Dec 9, 2023

Closes #70656
Closes #69988
Closes #69626 (I didn't see which types expose a well-known AddRange(ReadOnlySpan) though.)

  • Convert a collection-expr of the form [..expr] to expr.ToArray() when possible.
    • When expr is List<T>, using List<T>.ToArray()
    • Otherwise by converting expr to Span/ReadOnlySpan then calling ToArray
  • Convert a collection-expr of the form [elems, ..expr] to use expr.CopyTo(dest) when possible.
    • Converting dest to Span<T>
    • Converting expr to Span<T> or ReadOnlySpan<T>
  • Convert a collection-expr of the form [elems, ..expr] to dest.AddRange(expr) for List<T> when CollectionsMarshal is not available
    • Converting expr to IEnumerable<T> (only if it is an implicit reference conversion)

Perf data for these various strategies can be found here:
(for arrays) #70656 (comment)
(for lists) #70656 (comment)

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Dec 9, 2023
@CyrusNajmabadi
Copy link
Member

This is awesome.

I think it would be good to have a simple doc that lists the patterns we optimize, and the version of the compiler (maybe) that introduced the optimization. It would be good for people who want to know when this stuff does or doesn't happen.

@RikkiGibson
Copy link
Contributor Author

It looks like a change in the base branch is preventing optimized codegen in 7 tests in CollectionExpressionTests, and I forgot to update a VB WellKnownMembers test. I'll look into it tomorrow.

@cston

This comment was marked as resolved.

@RikkiGibson RikkiGibson requested a review from cston December 15, 2023 06:11
@cston
Copy link
Member

cston commented Dec 15, 2023

        verifier.VerifyIL("C.Main", """

Should this verify C.<Main>d__1.System.Runtime.CompilerServices.IAsyncStateMachine.MoveNext() instead? #Closed


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:27100 in cce39d0. [](commit_id = cce39d0, deletion_comment = False)

@RikkiGibson RikkiGibson requested a review from a team December 15, 2023 18:57
Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done review pass

}
}

MethodSymbol? tryGetToArrayMethod(TypeSymbol spreadTypeOriginalDefinition, WellKnownType wellKnownType, WellKnownMember wellKnownMember)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really a try-get in traditional form, and it feels like the code that uses it is a bit convoluted because of it. Consider refactoring to be a standard tryGet with an out parameter.

@@ -599,6 +636,20 @@ private BoundExpression CreateAndPopulateArray(BoundCollectionExpression node, A
_factory.Binary(BinaryOperatorKind.Addition, indexTemp.Type, indexTemp, _factory.Literal(1)),
isRef: false,
indexTemp.Type));
},
(sideEffects, arrayTemp, spreadElement, rewrittenSpreadOperand) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider naming these delegate parameters, especially now that there are multiple of them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, please spell out the types of these parameters. Same goes for the other similar delegates below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure what you meant by name the delegate parameters. Were you suggesting that I declare a named delegate type which has these parameter names?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean specify the parameter name for each of these delegate arguments. What is the parameter this delegate is being passed to?

/// Returns true if type is convertible to Span or ReadOnlySpan.
/// If non-identity conversion, also returns a non-null asSpanMethod.
/// </summary>
private bool TryGetSpanConversion(TypeSymbol type, out MethodSymbol? asSpanMethod)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rearrange this method to simplify things. First, check to see if it's the array case. The rest of the cases depend on it being a named type symbol, so you can do that check once and bail if false, rather than type-checking against NamedTypeSymbol 3 times.

return false;
}

private BoundExpression? TryConvertToSpanOrReadOnlySpan(BoundExpression expression)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like this is another method that would be better served by following the standard TryGet pattern.

&& _factory.WellKnownMethod(getLengthMember, isOptional: true) is { } getLengthMethod
&& _factory.WellKnownMethod(copyToMember, isOptional: true) is { } copyToMethod)
{
return (getLengthMethod!, copyToMethod!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need suppressions on this line?

if (type is ArrayTypeSymbol { IsSZArray: true } arrayType
&& _factory.WellKnownMethod(WellKnownMember.System_Span_T__ctor_Array, isOptional: true) is { } spanCtorArray)
{
asSpanMethod = spanCtorArray.AsMember(spanCtorArray.ContainingType.Construct(arrayType.ElementType));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the element type is something that can't be a valid type parameter, like a pointer? Do we have a test covering that?

if (type is ArrayTypeSymbol { IsSZArray: true } arrayType
&& _factory.WellKnownMethod(WellKnownMember.System_Span_T__ctor_Array, isOptional: true) is { } spanCtorArray)
{
asSpanMethod = spanCtorArray.AsMember(spanCtorArray.ContainingType.Construct(arrayType.ElementType));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we know the element type is always going to be a valid type argument? Can we assert something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be fine with asserting, for example, that element type is implicitly convertible to object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a faulty assumption. The conversion to object is now checked in retail builds and if it isn't what we expect then we don't use either the ToArray or CopyTo optimization paths.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test that hits this? (It looks like the existing tests were unchanged, other than setting the target framework.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests require the object conversion check in order to work properly.

@@ -599,6 +637,19 @@ private BoundExpression CreateAndPopulateArray(BoundCollectionExpression node, A
_factory.Binary(BinaryOperatorKind.Addition, indexTemp.Type, indexTemp, _factory.Literal(1)),
isRef: false,
indexTemp.Type));
},
(ArrayBuilder<BoundExpression> sideEffects, BoundExpression arrayTemp, BoundCollectionExpressionSpreadElement spreadElement, BoundExpression rewrittenSpreadOperand) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still missing tryOptimizeSpreadElement: naming.

@RikkiGibson RikkiGibson requested a review from 333fred December 15, 2023 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
5 participants