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

Optimize single spread collection expression for List<T> #74769

Merged
merged 21 commits into from
Sep 3, 2024

Conversation

DoctorKrolic
Copy link
Contributor

@DoctorKrolic DoctorKrolic commented Aug 15, 2024

Closes: #71217

So, perhaps heuristic should instead be: use ToList() if spread operand implements ICollection<T> or the compile-time type lacks a struct enumerator.

@DoctorKrolic DoctorKrolic requested a review from a team as a code owner August 15, 2024 11:38
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Aug 15, 2024
@dotnet-policy-service dotnet-policy-service bot added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Aug 15, 2024
[Theory]
[InlineData(TargetFramework.Net80)]
[InlineData(TargetFramework.Standard)]
public void List_SingleSpread_CustomCollection_ICollectionAndStructEnumerator(TargetFramework targetFramework)
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 original motivation for verifying multiple target frameworks was to test AddRange code path. Since now there is no difference between codegen on .NET Framework and modern .NET, I don't see value in having multiple TFMs to verify against

@@ -35272,20 +35316,20 @@ static void Main()
{
IEnumerable<int> e = [1, 2, 3];
e.Report();
List<int> list = [..e];
List<int> list = [..e, 4];
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 didin't want to mix single-spread and AddRange tests together and I wanted to be sure, that AddRange code paths are still being verified. Therefore I modified existing AddRange tests, but added an equivalent SingleSpread tests on top for these cases

@@ -19501,7 +19433,7 @@ static void Main()
var x = F([1, 2, 3]);
x.Report();
}
static List<T> F<T>(T[] items) => [..items];
static List<T> F<T>(T[] items) => [..items, ..items];
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 test for missing members doesn't make sense with new codegen, thus modified

@@ -10048,18 +10048,13 @@ static void Main()
}
""";

var verifier = CompileAndVerify(new[] { source, s_collectionExtensions }, expectedOutput: IncludeExpectedOutput("[1, 2, 3], [1, 2, 3],"), targetFramework: TargetFramework.Net80, verify: Verification.Skipped);
var verifier = CompileAndVerify(new[] { source, s_collectionExtensions }, expectedOutput: "[1, 2, 3], [1, 2, 3],", verify: Verification.Skipped);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All changed tests can now use standard TFM, so IncludeExpectedOutput is no longer needed

[Fact]
public void List_SingleSpread_CustomCollection_NotICollectionAndNoStructEnumerator()
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71217")]
public void List_SingleSpread_IEnumerable()
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 diff is a mess, I don't know why is that. I just added a chunk of SingleSpread tests above SingleSpread_CustomCollection cases

}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71217")]
public void List_SingleSpread_IEnumerable_ClassConstraint()
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 unconstrained and struct-constrained cases are considered as boxing conversions, therefore they are verified by List_AddRange_IEnumerable_Constraint test

@RikkiGibson RikkiGibson self-requested a review August 15, 2024 16:22

var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;

var conversion = _compilation.Conversions.ClassifyImplicitConversionFromType(spreadType, iEnumerableOfElementType, ref discardedUseSiteInfo);
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 ICollection<T> check, the resulting conversion is kinda used in the final codegn, although implicitly

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

LGTM except some minor comments


var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;

var conversion = _compilation.Conversions.ClassifyImplicitConversionFromType(spreadType, iEnumerableOfElementType, ref discardedUseSiteInfo);
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

@DoctorKrolic
Copy link
Contributor Author

It looks like there may be legit bootstrap build failures

Unfortunatelly yes, bootstrap failure seem to consistently reproduce in CI and I was able to reproduce the failure locally. Also it is a "bad" type of failure where no Debug.Assert was hit and the error with its stack trace look like this:

ID=MSBuild 5396 TID=38: Error Error: 'EndOfStreamException' 'Reached end of stream before end of read.' occurred during 'Reading response for Microsoft.CodeAnalysis.Features (net7.0)'
Stack trace:
   at Microsoft.CodeAnalysis.CommandLine.BuildProtocolConstants.<ReadAllAsync>d__4.MoveNext() in /_/src/Compilers/Core/CommandLine/BuildProtocol.cs:line 621
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.CodeAnalysis.CommandLine.BuildResponse.<ReadAsync>d__5.MoveNext() in /_/src/Compilers/Core/CommandLine/BuildProtocol.cs:line 322
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.CodeAnalysis.CommandLine.BuildServerConnection.<<RunServerBuildRequestAsync>g__tryRunRequestAsync|8_1>d.MoveNext() in /_/src/Compilers/Shared/BuildServerConnection.cs:line 312

There are multiple error like this, some have suspisious "Unnamed compilation" in the message, e.g. ID=MSBuild 7328 TID=47: Error Error: 'EndOfStreamException' 'Reached end of stream before end of read.' occurred during 'Reading response for Unnamed compilation 9780962c-3581-4fe9-b678-bc7725994f7d'

Let us know if you need assistance investigating the failures.

Given that the stack trace doesn't show anything related to the changes of the PR and no Debug.Assert was hit (therefore I cannot attach to the process at the point when something goes wrong) I cannot find out what is the point of failure myself and need external help(

@cston
Copy link
Member

cston commented Aug 29, 2024

Please test the following, where M1() and M2() should use foreach, and M3() and M4() should use Enumerable.ToList<U>(IEnumerable<U>).

using System.Collections.Generic;
interface IMyEnumerable<T> : IEnumerable<T>
{
    new MyEnumerator<T> GetEnumerator();
}
interface IMyCollection<T> : ICollection<T>
{
    new MyEnumerator<T> GetEnumerator();
}
struct MyEnumerator<T>
{
    public T Current => default;
    public bool MoveNext() => false;
}
class Program
{
    static void Main()
    {
    }
#nullable enable
    static List<U> M1<T, U>(T c)
        where T : class, IMyEnumerable<U>
        where U : class
    {
        return [..c];
    }
    static List<U?> M2<T, U>(T c)
        where T : class, IMyEnumerable<U>
        where U : class
    {
        return [..c];
    }
    static List<U> M3<T, U>(T c)
        where T : class, IMyCollection<U>
        where U : class
    {
        return [..c];
    }
    static List<U?> M4<T, U>(T c)
        where T : class, IMyCollection<U>
        where U : class
    {
        return [..c];
    }
}

@cston
Copy link
Member

cston commented Aug 29, 2024

Unfortunatelly yes, bootstrap failure seem to consistently reproduce in CI and I was able to reproduce the failure locally.

I ran build -bootstrap -config Debug and attached to VBCSCompiler.exe after the bootstrap build was created and started. There was an unhandled exception compiling Microsoft.CodeAnalysis.Features.dll compiling this code.

System.InvalidOperationException: Unexpected value 'PropertyAccess' of type 'Microsoft.CodeAnalysis.CSharp.BoundKind'

Microsoft.CodeAnalysis.CSharp.dll!Microsoft.CodeAnalysis.CSharp.CodeGen.CodeGenerator.EmitExpressionCore(Microsoft.CodeAnalysis.CSharp.BoundExpression expression, bool used) Line 357 C#
...

@cston
Copy link
Member

cston commented Aug 30, 2024

Please add the test from #74769 (comment).

@DoctorKrolic
Copy link
Contributor Author

@cston Please merge

@cston
Copy link
Member

cston commented Sep 2, 2024

@cston Please merge

Sorry for the delay. I wanted to give @RikkiGibson a chance to review the last two commits.

@cston cston merged commit c6a0795 into dotnet:main Sep 3, 2024
24 checks passed
@cston
Copy link
Member

cston commented Sep 3, 2024

Thanks @DoctorKrolic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee. untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use ToList for collection-exprs of a single spread
6 participants