-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Add ArrayBuilder.AddRange and use it instead of List.ToArray pattern #122209
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
base: main
Are you sure you want to change the base?
Conversation
…Info, and Timer to use it Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com>
Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com>
src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs
Show resolved
Hide resolved
Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds an AddRange method to ArrayBuilder<T> to improve performance in locations that previously used the new List<T>(enumerable).ToArray() pattern. The implementation optimizes for ICollection<T> inputs by pre-allocating capacity, while falling back to incremental growth for general IEnumerable<T> inputs.
Key changes:
- Added
AddRange(IEnumerable<T>)method to both Common and NativeAOTArrayBuilder<T>implementations withICollection<T>optimization - Refactored I/O methods in
Directory.cs,DirectoryInfo.cs, andTimer.csto useArrayBuilder<T>.AddRange()instead ofList<T>.ToArray() - Added comprehensive test coverage including tests for the ICollection optimization path, empty collections, and incremental growth
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs | Adds AddRange method with ICollection optimization for pre-allocating capacity |
| src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs | Adds AddRange method to NativeAOT's ArrayBuilder with same optimization |
| src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs | Refactors three methods (GetFiles, GetDirectories, GetFileSystemEntries) to use ArrayBuilder.AddRange instead of List.ToArray pattern |
| src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs | Refactors three methods (GetFiles, GetFileSystemInfos, GetDirectories) to use ArrayBuilder.AddRange instead of List.ToArray pattern |
| src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs | Refactors debugger proxy property to use ArrayBuilder.AddRange instead of List.ToArray pattern |
| src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs | Adds comprehensive test coverage for AddRange including ICollection optimization, empty collections, and non-ICollection paths |
|
|
||
| [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] | ||
| public TimerQueueTimer[] Items => new List<TimerQueueTimer>(_queue.GetTimersForDebugger()).ToArray(); | ||
| public TimerQueueTimer[] Items |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in a debugger type proxy.... the extra allocations here don't matter.... better to keep the code simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot please make this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted Timer.cs to use the original List approach in 9491f52.
| foreach (T item in collection) | ||
| { | ||
| UncheckedAdd(item); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a deoptimization over using List, as it's copying element by element via enumeration rather than using ICollection.CopyTo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot please make this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to use ICollection.CopyTo instead of element-by-element enumeration in 9491f52. Both the Common and NativeAOT ArrayBuilder implementations now use CopyTo for better performance.
| { | ||
| Debug.Assert(items != null); | ||
|
|
||
| if (items is ICollection<T> collection) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@333fred, all of the cited uses won't have an ICollection, as they all appear to be iterators. Are there other callers where this code will be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There aren't currently. However, if AddRange is an overload that is offered on ArrayBuilder, then it seems likely that at some point, someone will try to call it with an ICollection; while I didn't suggest that Copilot add this optimization, it seems like a reasonable thing to keep (assuming, of course, it uses CopyTo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The internal helper types like ArrayBuilder are implemented to be minimal. They are often not behaving like you would expect from a public BCL API and they tend to expose methods to squeeze bits of extra performance (e.g. ArrayBuilder.UncheckedAdd).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to go either way here. Just tell whether you want me to keep the optimization around ICollection or not 🙂.
|
@333fred, I'm wondering if the additional complexity here is warranted. The possible allocation savings are:
Presumably the latter rarely occurs, so this is primarily about avoiding the list object on code paths that are already allocating a bunch of FileSystemInfo objects and an array to store them all? |
Right. Perfectly fine if we want to just keep the |
Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com>
Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com>
I'm not sure the allocation savings are particularly meaningful, except maybe in some more niche edge cases. I think the bigger thing is avoiding repeated copies by ensuring that the backing collection is big enough. Allocations really aren't that expensive. The GC is also very good at its job, we have plenty of analyzers and tools to detect allocations when they do get meaningful, etc. The copy cost, however, can be much more important as it can impact the rest of the program due to how it invalidates the cache and involves touching two or more times the memory than needed. I think this is more what makes this a meaningful thing to handle The backing collection isn't likely to be the right size, so at least one copy is likely needed. But there's still all the intermediate growth copies that happen in the interim as we go |
How does this meaningfully help avoid copies? There's no difference here in presizing, and the only time a copy will be avoided is if the resulting array happens to be exactly the same size as what's required, which is unlikely. |
AddRangemethod toArrayBuilder<T>in/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.csDirectory.csto useArrayBuilder<T>withAddRangeinstead ofnew List<T>(enumerable).ToArray()DirectoryInfo.csto useArrayBuilder<T>withAddRangeinstead ofnew List<T>(enumerable).ToArray()ArrayBuilder<T>.AddRangemethodAddRangeto pre-allocate forICollection<T>inputs and useICollection.CopyTofor better performanceAddRangemethod to NativeAOT'sArrayBuilder<T>in/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.csEnsureCapacitytoGrowin Common ArrayBuilder for clarity (the method expects capacity checks before calling)ICollection<T>optimization:AddRange_ICollection_PreallocatesCapacity- verifies capacity is pre-allocated for ICollection inputsAddRange_ICollection_EmptyCollection- verifies empty ICollection works correctlyAddRange_ICollection_AfterExistingItems- verifies ICollection optimization works when items already existAddRange_NonICollection_GrowsIncrementally- verifies non-ICollection path grows incrementallyOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.