-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Introduce size-optimized IListSelect iterator #118156
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
Conversation
This lets us keep some of the constant-time indexing advantages of the IList iterator, without the GVM overhead of Select. There is a small size increase here, but nowhere near the cost of the GVM. In a pathological generated example for GVMs the cost was: 1. .NET 9: 12 MB 2. .NET 10 w/out this change: 2.2 MB 3. .NET 10 w/ this change: 2.3 MB In a real-world example (AzureMCP), the size attributed to System.Linq was: 1. .NET 9: 1.2 MB 2. .NET 10 w/out this change: 340 KB 3. .NET 10 w/ this change: 430 KB This seems like a good tradeoff. We mostly keep the algorithmic complexity the same across the size/speed-opt versions, and just tradeoff on the margins. We could probably continue to improve this in the future.
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 introduces a size-optimized IList-based Select iterator to reduce binary size while maintaining constant-time indexing advantages. The change removes the size-optimized variants of Take and Skip operations in favor of a more targeted optimization for Select operations on IList collections.
Key changes:
- Introduces
SizeOptIListSelectIterator
for size-optimized Select operations on IList collections - Removes size-optimized Take and Skip iterator implementations
- Updates Take and Skip to always use speed-optimized variants
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
File | Description |
---|---|
src/libraries/System.Linq/src/System/Linq/Take.cs | Removes conditional size optimization, always uses speed-optimized iterator |
src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs | Removes size-optimized Take iterator implementation |
src/libraries/System.Linq/src/System/Linq/Skip.cs | Removes conditional size optimization, always uses speed-optimized iterator |
src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs | Completely removes file containing size-optimized Skip iterator |
src/libraries/System.Linq/src/System/Linq/Select.cs | Adds IList detection and uses new size-optimized IList iterator |
src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs | New file implementing size-optimized IList Select iterator |
src/libraries/System.Linq/src/System.Linq.csproj | Updates project to include new Iterator.SizeOpt.cs and remove Skip.SizeOpt.cs |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
/azp run runtime-nativeaot-outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
Co-authored-by: Stephen Toub <stoub@microsoft.com>
/azp run runtime-nativeaot-outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
@stephentoub Would you mind taking another look at this? I think it's in a good spot. |
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.
LGTM from a LINQ perspective if you're happy with it from a size perspective.
From native AOT perspective, the size optimized LINQ switch still provides enough benefit over non-size optimized to have a standing, so native AOT size LGTM if this provides the throughput improvements that are needed. |
Thanks everyone! It was a little hairy, but the final result looks pretty good to me.
I believe this is in a good place. There are some corner cases (set union) where there are still perf differences, but this heavily narrows the gap. |
/ba-g all failures known |
|
||
public SizeOptIListWhereIterator(IList<TSource> source, Func<TSource, bool> predicate) | ||
{ | ||
Debug.Assert(source is not null && source.Count > 0); |
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 guess the code of class SizeOptIListWhereIterator
was copied from ListWhereIterator
, but here an extra assertion of source.Count > 0
was added? Empty IList is not filtered out by caller.
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.
Nice catch! Yes, this was a copy-paste error. However, the code does correctly handle this case -- only the assert is incorrect. I'll fix it up in a follow-up PR.
/backport to release/10.0-rc1 |
Started backporting to release/10.0-rc1: https://github.com/dotnet/runtime/actions/runs/17031246103 |
This lets us keep some of the constant-time indexing advantages of the IList iterator, without the GVM overhead of Select. There is a small size increase here, but nowhere near the cost of the GVM.
In a pathological generated example for GVMs the cost was:
In a real-world example (AzureMCP), the size attributed to System.Linq was:
This seems like a good tradeoff. We mostly keep the algorithmic complexity the same across the size/speed-opt versions, and just tradeoff on the margins. We could probably continue to improve this in the future.
Fixes #115033