Repository navigation
Block copy in List AddRange(ReadOnlySpan) and CopyTo(Span) - #685
Merged
Merged
Conversation
AddRange added one element at a time without reserving capacity, and CopyTo copied through the indexer. Reserve capacity up front, and block copy larger spans.
This was referenced Oct 12, 2026
This was referenced Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
List<T>.AddRange(ReadOnlySpan<T>)andList<T>.CopyTo(Span<T>)(polyfilled below net8) worked one element at a time:AddRangecalledAddwithout reserving capacity, andCopyToread every element through the indexer.AddRangeList<T>, so repeated small appends stay amortized) before adding.List<T>.AddRange(ArraySegment<T>), a block copy. On .NET Framework and netstandard targets that is only done for primitiveT: Framework'sList<T>.AddRangecopies the collection through a temporary array first, which measured no better thanAddfor reference types.CopyToCollectionsMarshal.AsSpan(list).CopyTo(destination).List<T>.CopyTo(int, T[], int, int)into a pooled array and one span copy. Smaller lists keep the loop (renting costs more than it saves).Pooled arrays have their used part cleared before being returned unless
Tis a primitive. No behaviour change: an empty span is still a no-op that does not invalidate enumerators, and the destination-too-short check is unchanged and still runs before anything is written.Measurements
Release, x64, Stopwatch, best of 15 runs, ns per call.
AddRangeis into a new empty list. Shared machine, so the small sizes are within noise.net48:
The .NET (Core) code path (what net5 to net7 consumers get), measured on the net8 runtime since older runtimes are not installed here:
Tests
Added to
PolyfillTests_List.cs:AddRangecompared withList<T>.AddRange(IEnumerable<T>)for sizes either side of the block copy threshold (0, 1, 63, 64, 65, 5000), forint,string?with nulls and a non-primitive struct; repeated appends of mixed sizes; growth staying amortized; appending a list's own span; enumerator invalidation (none for an empty span).CopyTofor the same sizes with spare list capacity, an offset and longer destination (head and tail untouched), reference types, and the too-short check on the large path.