Skip to content

Use CollectionsMarshal to get span from list in collection expression - #85007

Merged
RikkiGibson merged 13 commits into
dotnet:mainfrom
huoyaoyuan:spread-ros
Sep 3, 2026
Merged

Use CollectionsMarshal to get span from list in collection expression#85007
RikkiGibson merged 13 commits into
dotnet:mainfrom
huoyaoyuan:spread-ros

Conversation

@huoyaoyuan

@huoyaoyuan huoyaoyuan commented Aug 24, 2026

Copy link
Copy Markdown
Member

Fixes #75863.

Microsoft Reviewers: Open in CodeFlow

Copilot AI lite review requested due to automatic review settings August 24, 2026 07:40
@huoyaoyuan
huoyaoyuan requested a review from a team as a code owner August 24, 2026 07:40
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@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 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates C# collection-expression lowering when the target is Span<T>/ReadOnlySpan<T> and the elements’ length can’t be determined up front, aiming to avoid the extra List<T>.ToArray() allocation/copy by using CollectionsMarshal.AsSpan.

Changes:

  • Adds a new lowering path for unknown-length span targets that uses CollectionsMarshal.AsSpan(list) (when available) instead of list.ToArray() + span ctor.
  • Factors out shared “try optimized array” logic for array/span backing storage decisions.
  • Extends well-known member support to include the Span<T> -> ReadOnlySpan<T> implicit conversion operator and updates IL baselines accordingly.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs Uses CollectionsMarshal.AsSpan for unknown-length span targets (with fallback to ToArray) and shares optimized-array selection logic.
src/Compilers/Core/Portable/WellKnownMembers.cs Adds signature/name metadata for System_Span_T__op_Implicit_ReadOnlySpan_T.
src/Compilers/Core/Portable/WellKnownMember.cs Adds the corresponding WellKnownMember enum entry.
src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs Updates expected IL to reflect the new span-lowering shape that uses CollectionsMarshal.AsSpan.

{
array = CreateAndPopulateArray(node, arrayType);
}
// https://github.com/dotnet/roslyn/issues/68785: Emit Enumerable.TryGetNonEnumeratedCount() and avoid intermediate List<T> at runtime.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not sure if this comment is needed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ah, you were trying to preserve this comment as a move. tha't fine then.


var wellKnownMember = isReadOnlySpan ? WellKnownMember.System_ReadOnlySpan_T__ctor_Array : WellKnownMember.System_Span_T__ctor_Array;
var spanConstructor = _factory.WellKnownMethod(wellKnownMember).AsMember(spanType);
if (tryCreateOptimizedArray(node, arrayType, isReadOnlySpan) is { } optimizedArrayValue)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'd prefer the standard bool+out-var approach.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The code patterns (tryXXX returning nullable, is { }) were replicating existing patterns in the enclosing method. Should they be updated individually?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In that case, best to keep consistent with the surrounding code :-). Thanks!

return wrapArrayInSpan(array, spanType, isReadOnlySpan);
}

BoundExpression wrapArrayInSpan(BoundExpression arrayValue, NamedTypeSymbol spanType, bool isReadOnlySpan)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you make this static? it's unclear to me what this is capturing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's capturing the instance _factory from this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Those can be pass in (including passing in 'this'). For my, that's preferable as it makes it clear what it is not capturing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There's also _compilation used for assertion. I haven't found existing practice for explicitly passing _factory.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can pass in 'this'

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm still unsure about this. All of the local functions in this method were capturing this only.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think if we wrote it fresh today, we would probably use static and explicit @this parameter. But aligning with the pattern in this area is fine.

// Array/Span collections cannot have with-elements. So we can create a List<T> in an optimal
// fashion depending on our analysis of the elements.
rewrittenReceiver: null);
Debug.Assert(list.Type is { });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you just say is not null?

Debug.Assert(list.Type is { });
Debug.Assert(list.Type.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_Collections_Generic_List_T), TypeCompareKind.AllIgnoreOptions));

if (_factory.WellKnownMethod(WellKnownMember.System_Runtime_InteropServices_CollectionsMarshal__AsSpan_T, isOptional: true) is { } asSpanMethod)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you invert this, so the simple case is easy to see right away, then the more compelx case.

consider a small amount of docs here indicating what actual code pattern this is generating.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Currently the code is ordered by priority. If BCL introduced more optimized helper in the future, where should it be placed then? if (helper1) {} else if (helper2) {} else { fallback } looks natural to me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That works for me. Consider doc'ing that. Also consider breaking this into an easy set of calls for each case to a local function for each case. Will make the ordering clear.

// of the base type, while usually such conversion requires stloc+ldloc with the local of the base type
assertTypesAreCompatible(_compilation, arrayType, spanConstructor.Parameters[0].Type, isReadOnlySpan);
return _factory.New(spanConstructor, arrayValue);
Debug.Assert(list.Type is { });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same is not null.

{
var implicitOperator = _factory.WellKnownMethod(WellKnownMember.System_Span_T__op_Implicit_ReadOnlySpan_T).AsMember((NamedTypeSymbol)listSpanValue.Type);
listSpanValue = _factory.Call(null, implicitOperator, listSpanValue);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

makes sense, but docs ould be good.

@@ -17138,35 +17138,37 @@ static MyCollection<object> F2(MyCollection<object> c)
verifier.VerifyIL("Program.F2",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

surprised there are so few test changes.

note: we should have tests that hit all teh interesting code paths here. for example, assigning to span vs ros.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I didn't find any dedicated test for this path, only indirect usage via collection builder. I can add more dedicated tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes please

Copilot AI review requested due to automatic review settings August 28, 2026 03:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings August 28, 2026 05:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

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

  • The List type assertions here are redundant with the immediately preceding asserts (lines 395-397). Keeping only one set reduces noise without changing debug coverage.
                    Debug.Assert(list.Type is { });
                    Debug.Assert(list.Type.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_Collections_Generic_List_T), TypeCompareKind.AllIgnoreOptions));

src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs:37369

  • Same as above: add a WorkItem link for traceability back to the tracked issue.

""");
}

[Fact]
Copilot AI review requested due to automatic review settings August 28, 2026 13:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes compiler lowering and the well-known member tables (high regression impact) and should get final confirmation from a human maintainer despite the added coverage.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@333fred 333fred left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@dotnet/roslyn-compiler for a second review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

AsSpan may expose an array previously passed to user-controlled ICollection<T>.CopyTo, enabling later mutation of the resulting span.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (4)

src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs:37369

  • This issue-linked regression test should carry the originating WorkItem URL, as required by .github/memory/TESTING_STRATEGY.md:29-31.
    src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs:37420
  • This issue-linked regression test should carry the originating WorkItem URL, as required by .github/memory/TESTING_STRATEGY.md:29-31.
    src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs:37501
  • This issue-linked regression test should carry the originating WorkItem URL, as required by .github/memory/TESTING_STRATEGY.md:29-31.
    src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs:37468
  • This issue-linked regression test should carry the originating WorkItem URL, as required by .github/memory/TESTING_STRATEGY.md:29-31.
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +400 to +401
// Use CollectionsMarshal.AsSpan(list) to avoid copying the underlying array.
// The array is exclusively used by the list and can be safely acquired.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, this is a complicated scenario, but it does seem like it's accurate that this is a concern. What's a bit unclear to me is whether an ICollection<T>.CopyTo behaving this way would be considered "invalid" behavior; after all, List<T> effectively already has this dependency that ICollection<T>.CopyTo doesn't actually reference the array, or the version invariants of List<T> wouldn't hold.

@RikkiGibson RikkiGibson Sep 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

a custom spread can retain this array and mutate the returned Span/ReadOnlySpan after the collection expression completes

I think there's no need to defend against this. A reasonable CopyTo impl would not do this. If needed we can adjust spec to indicate that such CopyTo is ill-formed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We're not even using CopyTo directly: it's List<T> that does it as an optimization. We're just calling an AddRange method on List<T>. So that's my inclination as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If CopyTo called by List<T>.AddRange is unreliable, ToArray in LINQ would also be unreliable because it also calls ICollection<T>.CopyTo on the result array.

Comment thread src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs Outdated
@RikkiGibson
RikkiGibson self-requested a review September 2, 2026 19:00
var list = CreateAndPopulateList(node, elementType, node.Elements,
// Array/Span collections cannot have with-elements. So we can create a List<T> in an optimal
// fashion depending on our analysis of the elements.
rewrittenReceiver: null);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Refreshing myself on this. The 'rewrittenReceiver' is basically the result of the 'with()' element. We know we don't have that so we just pass null here and let 'CreateAndPopulateList' automatically cook up a receiver (e.g. list instance). Right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I have't deep-dived about this and just copied from createArray. It should be the same situation for creating list.

""";

var comp = CreateCompilation(new[] { source, s_collectionExtensionsWithSpan }, options: TestOptions.ReleaseExe, targetFramework: TargetFramework.Net80);
comp.MakeMemberMissing(WellKnownMember.System_Runtime_InteropServices_CollectionsMarshal__AsSpan_T);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be good to verify the behavior when both this and the Span->ReadOnlySpan operator are missing.
It looks like we would give an error when only the operator is missing, but not when both are missing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added a test path for this.

comp.VerifyEmitDiagnostics(
// (15,36): error CS0656: Missing compiler required member 'System.Span`1.op_Implicit'
// ReadOnlySpan<int> result = [..e1, ..e2];
Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "[..e1, ..e2]").WithArguments("System.Span`1", "op_Implicit").WithLocation(15, 36)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems reasonable. Any well formed Span type would have a conversion op to ReadOnlySpan.

It would also be an option to fallback to ToArray+ReadOnlySpan..ctor here, but, it feels like that conversion op is fairly comparable to the ctor in terms of importance, if you don't have it you simply can't convert Spans to ReadOnlySpans, that's such a core functionality.

@RikkiGibson RikkiGibson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Let us know if you are planning to make any further changes per any of the PR feedback, and then we can merge

…iter_CollectionExpression.cs

Co-authored-by: Rikki Gibson <rikkigibson@gmail.com>
Copilot AI review requested due to automatic review settings September 3, 2026 01:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Low-level compiler lowering and emitted IL changes warrant maintainer review, and CI checks are still in progress.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 3, 2026 01:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused optimization includes appropriate fallback and missing-member coverage with no unresolved correctness issues.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@huoyaoyuan

huoyaoyuan commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

I have no further changes to make. You can check if there's still pending comment.

@RikkiGibson
RikkiGibson enabled auto-merge (squash) September 3, 2026 02:50
@RikkiGibson
RikkiGibson merged commit 6a0c2f2 into dotnet:main Sep 3, 2026
21 of 22 checks passed
@dotnet-policy-service dotnet-policy-service Bot added this to the Next milestone Sep 3, 2026
@huoyaoyuan
huoyaoyuan deleted the spread-ros branch September 3, 2026 03:19
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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Collection expr targeting Span type with spreads of unknown length performs unnecessary copies.

6 participants