Use CollectionsMarshal to get span from list in collection expression - #85007
Conversation
|
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. |
There was a problem hiding this comment.
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 oflist.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. |
There was a problem hiding this comment.
not sure if this comment is needed.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
i'd prefer the standard bool+out-var approach.
There was a problem hiding this comment.
The code patterns (tryXXX returning nullable, is { }) were replicating existing patterns in the enclosing method. Should they be updated individually?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
can you make this static? it's unclear to me what this is capturing.
There was a problem hiding this comment.
It's capturing the instance _factory from this.
There was a problem hiding this comment.
Those can be pass in (including passing in 'this'). For my, that's preferable as it makes it clear what it is not capturing
There was a problem hiding this comment.
There's also _compilation used for assertion. I haven't found existing practice for explicitly passing _factory.
There was a problem hiding this comment.
You can pass in 'this'
There was a problem hiding this comment.
I'm still unsure about this. All of the local functions in this method were capturing this only.
There was a problem hiding this comment.
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 { }); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { }); |
| { | ||
| var implicitOperator = _factory.WellKnownMethod(WellKnownMember.System_Span_T__op_Implicit_ReadOnlySpan_T).AsMember((NamedTypeSymbol)listSpanValue.Type); | ||
| listSpanValue = _factory.Call(null, implicitOperator, listSpanValue); | ||
| } |
There was a problem hiding this comment.
makes sense, but docs ould be good.
| @@ -17138,35 +17138,37 @@ static MyCollection<object> F2(MyCollection<object> c) | |||
| verifier.VerifyIL("Program.F2", | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I didn't find any dedicated test for this path, only indirect usage via collection builder. I can add more dedicated tests.
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
🔵 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
left a comment
There was a problem hiding this comment.
@dotnet/roslyn-compiler for a second review
There was a problem hiding this comment.
🟡 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
| // Use CollectionsMarshal.AsSpan(list) to avoid copying the underlying array. | ||
| // The array is exclusively used by the list and can be safely acquired. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
🟢 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
|
I have no further changes to make. You can check if there's still pending comment. |
Fixes #75863.
Microsoft Reviewers: Open in CodeFlow