From a63c26619de9238070d14030a9e8bdd74b605c23 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 28 Jul 2025 15:59:49 -0700 Subject: [PATCH 01/15] Introduce size-optimized IListSelect iterator 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. --- .../System.Linq/src/System.Linq.csproj | 2 +- .../src/System/Linq/Iterator.SizeOpt.cs | 104 ++++++++++++++++++ .../System.Linq/src/System/Linq/Select.cs | 5 + .../src/System/Linq/Skip.SizeOpt.cs | 20 ---- .../System.Linq/src/System/Linq/Skip.cs | 2 +- .../src/System/Linq/Take.SizeOpt.cs | 11 -- .../System.Linq/src/System/Linq/Take.cs | 2 +- 7 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs delete mode 100644 src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs diff --git a/src/libraries/System.Linq/src/System.Linq.csproj b/src/libraries/System.Linq/src/System.Linq.csproj index 2f93e584d18658..ebcda87a1c7377 100644 --- a/src/libraries/System.Linq/src/System.Linq.csproj +++ b/src/libraries/System.Linq/src/System.Linq.csproj @@ -36,6 +36,7 @@ + @@ -70,7 +71,6 @@ - diff --git a/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs new file mode 100644 index 00000000000000..ae2996d4aa5bea --- /dev/null +++ b/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace System.Linq; + +public static partial class Enumerable +{ + /// + /// An iterator that implements . This is used primarily in size-optimized + /// code to turn linear-time iterators into constant-time iterators. The primary cost is + /// additional type checks, which are small compared to generic virtual calls. + /// + private sealed class SizeOptIListSelectIterator(IList _source, Func _selector) + : Iterator, IList + { + TResult IList.this[int index] + { + get => _selector(_source[index]); + set => ThrowHelper.ThrowNotSupportedException(); + } + + int ICollection.Count => _source.Count; + bool ICollection.IsReadOnly => true; + + void ICollection.Add(TResult item) => ThrowHelper.ThrowNotSupportedException(); + void ICollection.Clear() => ThrowHelper.ThrowNotSupportedException(); + bool ICollection.Contains(TResult item) + { + for (int i = 0; i < _source.Count; i++) + { + if (EqualityComparer.Default.Equals(_selector(_source[i]), item)) + { + return true; + } + } + return false; + } + int IList.IndexOf(TResult item) + { + for (int i = 0; i < _source.Count; i++) + { + if (EqualityComparer.Default.Equals(_selector(_source[i]), item)) + { + return i; + } + } + return -1; + } + + void ICollection.CopyTo(TResult[] array, int arrayIndex) + { + for (int i = 0; i < _source.Count; i++) + { + array[arrayIndex + i] = _selector(_source[i]); + } + } + + void IList.Insert(int index, TResult item) => ThrowHelper.ThrowNotSupportedException(); + bool ICollection.Remove(TResult item) => ThrowHelper.ThrowNotSupportedException_Boolean(); + void IList.RemoveAt(int index) => ThrowHelper.ThrowNotSupportedException(); + + private protected override Iterator Clone() + => new SizeOptIListSelectIterator(_source, _selector); + + public override bool MoveNext() + { + // _state - 1 represents the zero-based index into the list. + // Having a separate field for the index would be more readable. However, we save it + // into _state with a bias to minimize field size of the iterator. + int index = _state - 1; + if ((uint)index <= (uint)_source.Count) + { + _current = _selector(_source[index]); + ++_state; + return true; + } + + Dispose(); + return false; + } + + public override TResult[] ToArray() + { + TResult[] array = new TResult[_source.Count]; + for (int i = 0; i < _source.Count; i++) + { + array[i] = _selector(_source[i]); + } + return array; + } + public override List ToList() + { + List list = new List(_source.Count); + for (int i = 0; i < _source.Count; i++) + { + list.Add(_selector(_source[i])); + } + return list; + } + public override int GetCount(bool onlyIfCheap) => _source.Count; + } +} diff --git a/src/libraries/System.Linq/src/System/Linq/Select.cs b/src/libraries/System.Linq/src/System/Linq/Select.cs index ac27c8dda22eeb..bfe06a0c580dd0 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices.Marshalling; using static System.Linq.Utilities; namespace System.Linq @@ -32,6 +33,10 @@ public static IEnumerable Select( // don't need more code, just more data structures describing the new types). if (IsSizeOptimized && typeof(TResult).IsValueType) { + if (source is IList il) + { + return new SizeOptIListSelectIterator(il, selector); + } return new IEnumerableSelectIterator(iterator, selector); } else diff --git a/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs deleted file mode 100644 index 13e6642ee1fc02..00000000000000 --- a/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace System.Linq -{ - public static partial class Enumerable - { - private static IEnumerable SizeOptimizedSkipIterator(IEnumerable source, int count) - { - using IEnumerator e = source.GetEnumerator(); - while (count > 0 && e.MoveNext()) count--; - if (count <= 0) - { - while (e.MoveNext()) yield return e.Current; - } - } - } -} diff --git a/src/libraries/System.Linq/src/System/Linq/Skip.cs b/src/libraries/System.Linq/src/System/Linq/Skip.cs index ac8252a07c6d0c..25424686b30a7f 100644 --- a/src/libraries/System.Linq/src/System/Linq/Skip.cs +++ b/src/libraries/System.Linq/src/System/Linq/Skip.cs @@ -35,7 +35,7 @@ public static IEnumerable Skip(this IEnumerable sourc return iterator.Skip(count) ?? Empty(); } - return IsSizeOptimized ? SizeOptimizedSkipIterator(source, count) : SpeedOptimizedSkipIterator(source, count); + return SpeedOptimizedSkipIterator(source, count); } public static IEnumerable SkipWhile(this IEnumerable source, Func predicate) diff --git a/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs index 6f2bd0d9b0fa6b..f4fab7c6b1d82c 100644 --- a/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs @@ -8,17 +8,6 @@ namespace System.Linq { public static partial class Enumerable { - private static IEnumerable SizeOptimizedTakeIterator(IEnumerable source, int count) - { - Debug.Assert(count > 0); - - foreach (TSource element in source) - { - yield return element; - if (--count == 0) break; - } - } - private static IEnumerable SizeOptimizedTakeRangeIterator(IEnumerable source, int startIndex, int endIndex) { Debug.Assert(source is not null); diff --git a/src/libraries/System.Linq/src/System/Linq/Take.cs b/src/libraries/System.Linq/src/System/Linq/Take.cs index 9df5fbc8a2bec8..17af029a7706e1 100644 --- a/src/libraries/System.Linq/src/System/Linq/Take.cs +++ b/src/libraries/System.Linq/src/System/Linq/Take.cs @@ -20,7 +20,7 @@ public static IEnumerable Take(this IEnumerable sourc return []; } - return IsSizeOptimized ? SizeOptimizedTakeIterator(source, count) : SpeedOptimizedTakeIterator(source, count); + return SpeedOptimizedTakeIterator(source, count); } /// Returns a specified range of contiguous elements from a sequence. From 32ef019f8b35af9c6ed82a04978ac1e3fab3e002 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 29 Jul 2025 08:49:26 -0700 Subject: [PATCH 02/15] Swap order --- src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs index ae2996d4aa5bea..1363f5025be766 100644 --- a/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs @@ -72,8 +72,8 @@ public override bool MoveNext() int index = _state - 1; if ((uint)index <= (uint)_source.Count) { - _current = _selector(_source[index]); ++_state; + _current = _selector(_source[index]); return true; } From 5f160df06de1fbd8be5c16aafe82b43e7ea3d608 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 29 Jul 2025 11:03:28 -0700 Subject: [PATCH 03/15] Fix condition --- .../src/System/Linq/Iterator.SizeOpt.cs | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs index 1363f5025be766..f562487177f70f 100644 --- a/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; namespace System.Linq; @@ -27,17 +28,11 @@ TResult IList.this[int index] void ICollection.Add(TResult item) => ThrowHelper.ThrowNotSupportedException(); void ICollection.Clear() => ThrowHelper.ThrowNotSupportedException(); bool ICollection.Contains(TResult item) - { - for (int i = 0; i < _source.Count; i++) - { - if (EqualityComparer.Default.Equals(_selector(_source[i]), item)) - { - return true; - } - } - return false; - } - int IList.IndexOf(TResult item) + => IndexOf(item) >= 0; + + int IList.IndexOf(TResult item) => IndexOf(item); + + private int IndexOf(TResult item) { for (int i = 0; i < _source.Count; i++) { @@ -66,14 +61,12 @@ private protected override Iterator Clone() public override bool MoveNext() { - // _state - 1 represents the zero-based index into the list. - // Having a separate field for the index would be more readable. However, we save it - // into _state with a bias to minimize field size of the iterator. + var source = _source; int index = _state - 1; - if ((uint)index <= (uint)_source.Count) + if ((uint)index < (uint)source.Count) { - ++_state; - _current = _selector(_source[index]); + _state++; + _current = _selector(source[index]); return true; } From ba6e84475b04fa4fcd2233fb5683c76fbdb05641 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 29 Jul 2025 13:33:03 -0700 Subject: [PATCH 04/15] Update src/libraries/System.Linq/src/System/Linq/Select.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/libraries/System.Linq/src/System/Linq/Select.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Select.cs b/src/libraries/System.Linq/src/System/Linq/Select.cs index bfe06a0c580dd0..a2a99193bde252 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices.Marshalling; using static System.Linq.Utilities; namespace System.Linq From 85cc7f6a4effbda34dd93a027165930f0eee19dc Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 30 Jul 2025 00:07:36 -0700 Subject: [PATCH 05/15] Rework to avoid implementing IList and just override Skip/Take --- .../System.Linq/src/System.Linq.csproj | 3 +- .../src/System/Linq/Select.SizeOpt.cs | 65 +++++++++++++++++++ .../System.Linq/src/System/Linq/Select.cs | 8 +-- .../src/System/Linq/Skip.SizeOpt.cs | 20 ++++++ .../System.Linq/src/System/Linq/Skip.cs | 4 +- .../src/System/Linq/Take.SizeOpt.cs | 11 ++++ .../System.Linq/src/System/Linq/Take.cs | 2 +- 7 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs create mode 100644 src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs diff --git a/src/libraries/System.Linq/src/System.Linq.csproj b/src/libraries/System.Linq/src/System.Linq.csproj index ebcda87a1c7377..3a0883c8d8d90f 100644 --- a/src/libraries/System.Linq/src/System.Linq.csproj +++ b/src/libraries/System.Linq/src/System.Linq.csproj @@ -36,7 +36,6 @@ - @@ -63,6 +62,7 @@ + @@ -71,6 +71,7 @@ + diff --git a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs new file mode 100644 index 00000000000000..3769828ffa3797 --- /dev/null +++ b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Linq +{ + public static partial class Enumerable + { + private sealed class SizeOptIListSelectIterator(IList _source, Func _selector) + : Iterator + { + public override int GetCount(bool onlyIfCheap) => _source.Count; + + public override Iterator Skip(int count) + { + Debug.Assert(count > 0); + return new IListSkipTakeSelectIterator(_source, _selector, count, int.MaxValue); + } + + public override Iterator Take(int count) + { + Debug.Assert(count > 0); + return new IListSkipTakeSelectIterator(_source, _selector, 0, count - 1); + } + + public override bool MoveNext() + { + var source = _source; + int index = _state - 1; + if ((uint)index < (uint)source.Count) + { + _state++; + _current = _selector(source[index]); + return true; + } + + Dispose(); + return false; + } + public override TResult[] ToArray() + { + TResult[] array = new TResult[_source.Count]; + for (int i = 0; i < _source.Count; i++) + { + array[i] = _selector(_source[i]); + } + return array; + } + public override List ToList() + { + List list = new List(_source.Count); + for (int i = 0; i < _source.Count; i++) + { + list.Add(_selector(_source[i])); + } + return list; + } + private protected override Iterator Clone() + => new SizeOptIListSelectIterator(_source, _selector); + } + } +} diff --git a/src/libraries/System.Linq/src/System/Linq/Select.cs b/src/libraries/System.Linq/src/System/Linq/Select.cs index a2a99193bde252..bfdc9a9e6ecff5 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.cs @@ -32,11 +32,9 @@ public static IEnumerable Select( // don't need more code, just more data structures describing the new types). if (IsSizeOptimized && typeof(TResult).IsValueType) { - if (source is IList il) - { - return new SizeOptIListSelectIterator(il, selector); - } - return new IEnumerableSelectIterator(iterator, selector); + return source is IList il + ? new SizeOptIListSelectIterator(il, selector) + : new IEnumerableSelectIterator(iterator, selector); } else { diff --git a/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs new file mode 100644 index 00000000000000..13e6642ee1fc02 --- /dev/null +++ b/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace System.Linq +{ + public static partial class Enumerable + { + private static IEnumerable SizeOptimizedSkipIterator(IEnumerable source, int count) + { + using IEnumerator e = source.GetEnumerator(); + while (count > 0 && e.MoveNext()) count--; + if (count <= 0) + { + while (e.MoveNext()) yield return e.Current; + } + } + } +} diff --git a/src/libraries/System.Linq/src/System/Linq/Skip.cs b/src/libraries/System.Linq/src/System/Linq/Skip.cs index 25424686b30a7f..5c75f23cc3de6a 100644 --- a/src/libraries/System.Linq/src/System/Linq/Skip.cs +++ b/src/libraries/System.Linq/src/System/Linq/Skip.cs @@ -30,12 +30,12 @@ public static IEnumerable Skip(this IEnumerable sourc count = 0; } - else if (!IsSizeOptimized && source is Iterator iterator) + else if (source is Iterator iterator) { return iterator.Skip(count) ?? Empty(); } - return SpeedOptimizedSkipIterator(source, count); + return IsSizeOptimized ? SizeOptimizedSkipIterator(source, count) : SpeedOptimizedSkipIterator(source, count); } public static IEnumerable SkipWhile(this IEnumerable source, Func predicate) diff --git a/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs index f4fab7c6b1d82c..6f2bd0d9b0fa6b 100644 --- a/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs @@ -8,6 +8,17 @@ namespace System.Linq { public static partial class Enumerable { + private static IEnumerable SizeOptimizedTakeIterator(IEnumerable source, int count) + { + Debug.Assert(count > 0); + + foreach (TSource element in source) + { + yield return element; + if (--count == 0) break; + } + } + private static IEnumerable SizeOptimizedTakeRangeIterator(IEnumerable source, int startIndex, int endIndex) { Debug.Assert(source is not null); diff --git a/src/libraries/System.Linq/src/System/Linq/Take.cs b/src/libraries/System.Linq/src/System/Linq/Take.cs index 17af029a7706e1..9df5fbc8a2bec8 100644 --- a/src/libraries/System.Linq/src/System/Linq/Take.cs +++ b/src/libraries/System.Linq/src/System/Linq/Take.cs @@ -20,7 +20,7 @@ public static IEnumerable Take(this IEnumerable sourc return []; } - return SpeedOptimizedTakeIterator(source, count); + return IsSizeOptimized ? SizeOptimizedTakeIterator(source, count) : SpeedOptimizedTakeIterator(source, count); } /// Returns a specified range of contiguous elements from a sequence. From de8bc77764a6a8344c7e60776a65d9860bf9400c Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 30 Jul 2025 14:21:57 -0700 Subject: [PATCH 06/15] Remove dead code --- .../src/System/Linq/Iterator.SizeOpt.cs | 97 ------------------- 1 file changed, 97 deletions(-) delete mode 100644 src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs diff --git a/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs deleted file mode 100644 index f562487177f70f..00000000000000 --- a/src/libraries/System.Linq/src/System/Linq/Iterator.SizeOpt.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace System.Linq; - -public static partial class Enumerable -{ - /// - /// An iterator that implements . This is used primarily in size-optimized - /// code to turn linear-time iterators into constant-time iterators. The primary cost is - /// additional type checks, which are small compared to generic virtual calls. - /// - private sealed class SizeOptIListSelectIterator(IList _source, Func _selector) - : Iterator, IList - { - TResult IList.this[int index] - { - get => _selector(_source[index]); - set => ThrowHelper.ThrowNotSupportedException(); - } - - int ICollection.Count => _source.Count; - bool ICollection.IsReadOnly => true; - - void ICollection.Add(TResult item) => ThrowHelper.ThrowNotSupportedException(); - void ICollection.Clear() => ThrowHelper.ThrowNotSupportedException(); - bool ICollection.Contains(TResult item) - => IndexOf(item) >= 0; - - int IList.IndexOf(TResult item) => IndexOf(item); - - private int IndexOf(TResult item) - { - for (int i = 0; i < _source.Count; i++) - { - if (EqualityComparer.Default.Equals(_selector(_source[i]), item)) - { - return i; - } - } - return -1; - } - - void ICollection.CopyTo(TResult[] array, int arrayIndex) - { - for (int i = 0; i < _source.Count; i++) - { - array[arrayIndex + i] = _selector(_source[i]); - } - } - - void IList.Insert(int index, TResult item) => ThrowHelper.ThrowNotSupportedException(); - bool ICollection.Remove(TResult item) => ThrowHelper.ThrowNotSupportedException_Boolean(); - void IList.RemoveAt(int index) => ThrowHelper.ThrowNotSupportedException(); - - private protected override Iterator Clone() - => new SizeOptIListSelectIterator(_source, _selector); - - public override bool MoveNext() - { - var source = _source; - int index = _state - 1; - if ((uint)index < (uint)source.Count) - { - _state++; - _current = _selector(source[index]); - return true; - } - - Dispose(); - return false; - } - - public override TResult[] ToArray() - { - TResult[] array = new TResult[_source.Count]; - for (int i = 0; i < _source.Count; i++) - { - array[i] = _selector(_source[i]); - } - return array; - } - public override List ToList() - { - List list = new List(_source.Count); - for (int i = 0; i < _source.Count; i++) - { - list.Add(_selector(_source[i])); - } - return list; - } - public override int GetCount(bool onlyIfCheap) => _source.Count; - } -} From 03e6df082882dbdbab84f2f29d146c8e965b68bc Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 1 Aug 2025 01:29:55 -0700 Subject: [PATCH 07/15] Update src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs Co-authored-by: Stephen Toub --- src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs index 3769828ffa3797..790c67c7ddf147 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs @@ -52,7 +52,7 @@ public override TResult[] ToArray() public override List ToList() { List list = new List(_source.Count); - for (int i = 0; i < _source.Count; i++) + for (int i = 0; i < list.Count; i++) { list.Add(_selector(_source[i])); } From 3cd96a40074d5d8058f92f2de1b0669135e4d6e5 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 1 Aug 2025 10:37:54 -0700 Subject: [PATCH 08/15] Update src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs Co-authored-by: Stephen Toub --- src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs index 790c67c7ddf147..7b4b7b36afb7c5 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs @@ -43,7 +43,7 @@ public override bool MoveNext() public override TResult[] ToArray() { TResult[] array = new TResult[_source.Count]; - for (int i = 0; i < _source.Count; i++) + for (int i = 0; i < array.Length; i++) { array[i] = _selector(_source[i]); } From 0286a5777e3fa5fa6c15284698f979754bf89a5f Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 1 Aug 2025 16:17:39 -0700 Subject: [PATCH 09/15] Respond to PR comments This takes a more aggressive direction and removes the size optimized versions of iterators for Skip and Take. As far as I can tell these are relatively small size increases, but using them preserves the O(1) optimizations in the ' speed' version. --- .../System.Linq/src/System.Linq.csproj | 2 - .../src/System/Linq/Select.SizeOpt.cs | 27 ++++++++++- .../src/System/Linq/Skip.SizeOpt.cs | 20 -------- .../System.Linq/src/System/Linq/Skip.cs | 2 +- .../src/System/Linq/Take.SizeOpt.cs | 47 ------------------- .../System.Linq/src/System/Linq/Take.cs | 8 ++-- src/libraries/System.Linq/tests/CountTests.cs | 2 +- .../System.Linq/tests/OrderedSubsetting.cs | 2 +- src/libraries/System.Linq/tests/RangeTests.cs | 2 +- .../System.Linq/tests/SelectTests.cs | 26 ++++++++++ src/libraries/System.Linq/tests/TakeTests.cs | 4 +- src/libraries/tests.proj | 1 + 12 files changed, 62 insertions(+), 81 deletions(-) delete mode 100644 src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs delete mode 100644 src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs diff --git a/src/libraries/System.Linq/src/System.Linq.csproj b/src/libraries/System.Linq/src/System.Linq.csproj index 3a0883c8d8d90f..baf71793f4dae9 100644 --- a/src/libraries/System.Linq/src/System.Linq.csproj +++ b/src/libraries/System.Linq/src/System.Linq.csproj @@ -71,12 +71,10 @@ - - diff --git a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs index 7b4b7b36afb7c5..d74d300c512719 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs @@ -12,7 +12,29 @@ public static partial class Enumerable private sealed class SizeOptIListSelectIterator(IList _source, Func _selector) : Iterator { - public override int GetCount(bool onlyIfCheap) => _source.Count; + public override int GetCount(bool onlyIfCheap) + { + // In case someone uses Count() to force evaluation of + // the selector, run it provided `onlyIfCheap` is false. + + if (onlyIfCheap) + { + return -1; + } + + int count = 0; + + foreach (TSource item in _source) + { + _selector(item); + checked + { + count++; + } + } + + return count; + } public override Iterator Skip(int count) { @@ -40,6 +62,7 @@ public override bool MoveNext() Dispose(); return false; } + public override TResult[] ToArray() { TResult[] array = new TResult[_source.Count]; @@ -49,6 +72,7 @@ public override TResult[] ToArray() } return array; } + public override List ToList() { List list = new List(_source.Count); @@ -58,6 +82,7 @@ public override List ToList() } return list; } + private protected override Iterator Clone() => new SizeOptIListSelectIterator(_source, _selector); } diff --git a/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs deleted file mode 100644 index 13e6642ee1fc02..00000000000000 --- a/src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace System.Linq -{ - public static partial class Enumerable - { - private static IEnumerable SizeOptimizedSkipIterator(IEnumerable source, int count) - { - using IEnumerator e = source.GetEnumerator(); - while (count > 0 && e.MoveNext()) count--; - if (count <= 0) - { - while (e.MoveNext()) yield return e.Current; - } - } - } -} diff --git a/src/libraries/System.Linq/src/System/Linq/Skip.cs b/src/libraries/System.Linq/src/System/Linq/Skip.cs index 5c75f23cc3de6a..4e76f1afba9794 100644 --- a/src/libraries/System.Linq/src/System/Linq/Skip.cs +++ b/src/libraries/System.Linq/src/System/Linq/Skip.cs @@ -35,7 +35,7 @@ public static IEnumerable Skip(this IEnumerable sourc return iterator.Skip(count) ?? Empty(); } - return IsSizeOptimized ? SizeOptimizedSkipIterator(source, count) : SpeedOptimizedSkipIterator(source, count); + return SpeedOptimizedSkipIterator(source, count); } public static IEnumerable SkipWhile(this IEnumerable source, Func predicate) diff --git a/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs deleted file mode 100644 index 6f2bd0d9b0fa6b..00000000000000 --- a/src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace System.Linq -{ - public static partial class Enumerable - { - private static IEnumerable SizeOptimizedTakeIterator(IEnumerable source, int count) - { - Debug.Assert(count > 0); - - foreach (TSource element in source) - { - yield return element; - if (--count == 0) break; - } - } - - private static IEnumerable SizeOptimizedTakeRangeIterator(IEnumerable source, int startIndex, int endIndex) - { - Debug.Assert(source is not null); - Debug.Assert(startIndex >= 0 && startIndex < endIndex); - - using IEnumerator e = source.GetEnumerator(); - - int index = 0; - while (index < startIndex && e.MoveNext()) - { - ++index; - } - - if (index < startIndex) - { - yield break; - } - - while (index < endIndex && e.MoveNext()) - { - yield return e.Current; - ++index; - } - } - } -} diff --git a/src/libraries/System.Linq/src/System/Linq/Take.cs b/src/libraries/System.Linq/src/System/Linq/Take.cs index 9df5fbc8a2bec8..8d6ad9acb3998d 100644 --- a/src/libraries/System.Linq/src/System/Linq/Take.cs +++ b/src/libraries/System.Linq/src/System/Linq/Take.cs @@ -20,7 +20,7 @@ public static IEnumerable Take(this IEnumerable sourc return []; } - return IsSizeOptimized ? SizeOptimizedTakeIterator(source, count) : SpeedOptimizedTakeIterator(source, count); + return SpeedOptimizedTakeIterator(source, count); } /// Returns a specified range of contiguous elements from a sequence. @@ -68,7 +68,7 @@ public static IEnumerable Take(this IEnumerable sourc return []; } - return IsSizeOptimized ? SizeOptimizedTakeRangeIterator(source, startIndex, endIndex) : SpeedOptimizedTakeRangeIterator(source, startIndex, endIndex); + return SpeedOptimizedTakeRangeIterator(source, startIndex, endIndex); } return TakeRangeFromEndIterator(source, isStartIndexFromEnd, startIndex, isEndIndexFromEnd, endIndex); @@ -94,9 +94,7 @@ private static IEnumerable TakeRangeFromEndIterator(IEnumerabl if (startIndex < endIndex) { - IEnumerable rangeIterator = IsSizeOptimized - ? SizeOptimizedTakeRangeIterator(source, startIndex, endIndex) - : SpeedOptimizedTakeRangeIterator(source, startIndex, endIndex); + IEnumerable rangeIterator = SpeedOptimizedTakeRangeIterator(source, startIndex, endIndex); foreach (TSource element in rangeIterator) { yield return element; diff --git a/src/libraries/System.Linq/tests/CountTests.cs b/src/libraries/System.Linq/tests/CountTests.cs index ddf96d4cf4b59a..e6a35f22e700b9 100644 --- a/src/libraries/System.Linq/tests/CountTests.cs +++ b/src/libraries/System.Linq/tests/CountTests.cs @@ -204,7 +204,7 @@ public static IEnumerable NonEnumeratedCount_UnsupportedEnumerables() if (!PlatformDetection.IsLinqSpeedOptimized) { yield return WrapArgs(Enumerable.Range(1, 50).Select(x => x + 1)); - yield return WrapArgs(new int[] { 1, 2, 3, 4 }.Select(x => x + 1)); + yield return WrapArgs(new int[] { 1, 2, 3, 4 }.Select(x => x + 1)); yield return WrapArgs(Enumerable.Range(1, 50).Select(x => x + 1).Select(x => x - 1)); yield return WrapArgs(Enumerable.Range(1, 20).Reverse()); yield return WrapArgs(Enumerable.Range(1, 20).OrderBy(x => -x)); diff --git a/src/libraries/System.Linq/tests/OrderedSubsetting.cs b/src/libraries/System.Linq/tests/OrderedSubsetting.cs index 5804ac1d4229e7..7826fb338dbce5 100644 --- a/src/libraries/System.Linq/tests/OrderedSubsetting.cs +++ b/src/libraries/System.Linq/tests/OrderedSubsetting.cs @@ -224,7 +224,7 @@ public void TakeAndSkip() Assert.Equal(Enumerable.Range(10, 1), ordered.Take(11).Skip(10)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsLinqSpeedOptimized))] + [Fact] public void TakeAndSkip_DoesntIterateRangeUnlessNecessary() { Assert.Empty(Enumerable.Range(0, int.MaxValue).Take(int.MaxValue).OrderBy(i => i).Skip(int.MaxValue - 4).Skip(15)); diff --git a/src/libraries/System.Linq/tests/RangeTests.cs b/src/libraries/System.Linq/tests/RangeTests.cs index 476d4804fefeef..0bb6acca8bc685 100644 --- a/src/libraries/System.Linq/tests/RangeTests.cs +++ b/src/libraries/System.Linq/tests/RangeTests.cs @@ -236,7 +236,7 @@ public void LastOrDefault() Assert.Equal(int.MaxValue - 101, GetRange(-100, int.MaxValue).LastOrDefault()); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsLinqSpeedOptimized))] + [Fact] public void IListImplementationIsValid() { Validate(GetRange(42, 10), [42, 43, 44, 45, 46, 47, 48, 49, 50, 51]); diff --git a/src/libraries/System.Linq/tests/SelectTests.cs b/src/libraries/System.Linq/tests/SelectTests.cs index c7c84565485c73..5fdae70d06df38 100644 --- a/src/libraries/System.Linq/tests/SelectTests.cs +++ b/src/libraries/System.Linq/tests/SelectTests.cs @@ -12,6 +12,32 @@ namespace System.Linq.Tests { public class SelectTests : EnumerableTests { + [Fact] + public void SelectSideEffectsExecutedOnCount() + { + int i = 0; + // If we made no promises about side effects, i could be 0, but in practice users have + // taken a dependency on side effects executing on Count. + var count = Enumerable.Range(1, 10).Select(x => i++).Count(); + Assert.Equal(10, count); + Assert.Equal(10, i); + + i = 0; + count = Enumerable.Range(1, 10).Skip(5).Select(x => i++).Count(); + Assert.Equal(5, count); + Assert.Equal(5, i); + + i = 0; + count = Enumerable.Range(1, 10).Take(5).Select(x => i++).Count(); + Assert.Equal(5, count); + Assert.Equal(5, i); + + i = 0; + count = Enumerable.Range(1, 10).Skip(2).Take(3).Select(x => i++).Count(); + Assert.Equal(3, count); + Assert.Equal(3, i); + } + [Fact] public void SameResultsRepeatCallsStringQuery() { diff --git a/src/libraries/System.Linq/tests/TakeTests.cs b/src/libraries/System.Linq/tests/TakeTests.cs index bcdaa42df0a9b5..310a942e507feb 100644 --- a/src/libraries/System.Linq/tests/TakeTests.cs +++ b/src/libraries/System.Linq/tests/TakeTests.cs @@ -669,7 +669,7 @@ public void RepeatEnumerating() } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsLinqSpeedOptimized))] + [Theory] [InlineData(1000)] [InlineData(1000000)] [InlineData(int.MaxValue)] @@ -1623,7 +1623,7 @@ public void EmptySource_DoNotThrowException_EnumerablePartition() Assert.Empty(EnumerablePartitionOrEmpty(source).Take(^6..^7)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsLinqSpeedOptimized))] + [Fact] public void SkipTakeOnIListIsIList() { IList list = new ReadOnlyCollection(Enumerable.Range(0, 100).ToList()); diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 64c690245b51c5..0549cf599f6091 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -577,6 +577,7 @@ + From d1de81fc9385977faab6a19d0bb7890c06526c87 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 8 Aug 2025 16:29:14 -0700 Subject: [PATCH 10/15] Optimize away IList, which is a lot of the size increase --- .../System.Linq/src/System/Linq/OfType.SpeedOpt.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs index 4420801ee4abcf..6bb4272feb7794 100644 --- a/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs @@ -177,7 +177,11 @@ public override IEnumerable Select(Func s public override bool Contains(TResult value) { - if (!typeof(TResult).IsValueType && // don't box TResult + // Avoid checking for IList when size-optimized because it keeps IList + // implementations which may otherwise be trimmed. Since List implements + // IList and List is popular, this could potentially be a lot of code. + if (!IsSizeOptimized && + !typeof(TResult).IsValueType && // don't box TResult _source is IList list) { return list.Contains(value); From eae31b624dcc5c523c6a2f5016ca6904e78073ed Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 9 Aug 2025 15:27:33 -0700 Subject: [PATCH 11/15] Remove more size-opt special cases These are more (relatively common) cases where you could end up with an O(n) implementation instead of O(1) even when the backing enumerable is capable of doing O(1) index access. --- src/libraries/System.Linq/src/System/Linq/Count.cs | 4 ++-- src/libraries/System.Linq/src/System/Linq/ElementAt.cs | 4 ++-- src/libraries/System.Linq/src/System/Linq/Last.cs | 2 +- src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs | 2 +- src/libraries/System.Linq/src/System/Linq/Select.cs | 5 +++++ 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Count.cs b/src/libraries/System.Linq/src/System/Linq/Count.cs index 6a12a11cbe163d..cd175cf6b9b3cc 100644 --- a/src/libraries/System.Linq/src/System/Linq/Count.cs +++ b/src/libraries/System.Linq/src/System/Linq/Count.cs @@ -20,7 +20,7 @@ public static int Count(this IEnumerable source) return collectionoft.Count; } - if (!IsSizeOptimized && source is Iterator iterator) + if (source is Iterator iterator) { return iterator.GetCount(onlyIfCheap: false); } @@ -113,7 +113,7 @@ public static bool TryGetNonEnumeratedCount(this IEnumerable s return true; } - if (!IsSizeOptimized && source is Iterator iterator) + if (source is Iterator iterator) { int c = iterator.GetCount(onlyIfCheap: true); if (c >= 0) diff --git a/src/libraries/System.Linq/src/System/Linq/ElementAt.cs b/src/libraries/System.Linq/src/System/Linq/ElementAt.cs index 26c69366fa9f3b..f4dc1f23a16c80 100644 --- a/src/libraries/System.Linq/src/System/Linq/ElementAt.cs +++ b/src/libraries/System.Linq/src/System/Linq/ElementAt.cs @@ -23,7 +23,7 @@ public static TSource ElementAt(this IEnumerable source, int i bool found; TSource? element = - !IsSizeOptimized && source is Iterator iterator ? iterator.TryGetElementAt(index, out found) : + source is Iterator iterator ? iterator.TryGetElementAt(index, out found) : TryGetElementAtNonIterator(source, index, out found); if (!found) @@ -121,7 +121,7 @@ public static TSource ElementAt(this IEnumerable source, Index } return - !IsSizeOptimized && source is Iterator iterator ? iterator.TryGetElementAt(index, out found) : + source is Iterator iterator ? iterator.TryGetElementAt(index, out found) : TryGetElementAtNonIterator(source, index, out found); } diff --git a/src/libraries/System.Linq/src/System/Linq/Last.cs b/src/libraries/System.Linq/src/System/Linq/Last.cs index ca48475259d8e5..c38604e397592f 100644 --- a/src/libraries/System.Linq/src/System/Linq/Last.cs +++ b/src/libraries/System.Linq/src/System/Linq/Last.cs @@ -69,7 +69,7 @@ public static TSource LastOrDefault(this IEnumerable source, F } return - !IsSizeOptimized && source is Iterator iterator ? iterator.TryGetLast(out found) : + source is Iterator iterator ? iterator.TryGetLast(out found) : TryGetLastNonIterator(source, out found); } diff --git a/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs index 6bb4272feb7794..f2649e706c8a0e 100644 --- a/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs @@ -167,7 +167,7 @@ public override IEnumerable Select(Func s // they're covariant. It's not worthwhile checking for List to use the ListWhereSelectIterator // because List<> is not covariant. Func isTResult = static o => o is TResult; - return objectSource is object[] array ? + return !IsSizeOptimized && objectSource is object[] array ? new ArrayWhereSelectIterator(array, isTResult, localSelector) : new IEnumerableWhereSelectIterator(objectSource, isTResult, localSelector); } diff --git a/src/libraries/System.Linq/src/System/Linq/Select.cs b/src/libraries/System.Linq/src/System/Linq/Select.cs index bfdc9a9e6ecff5..d5fcb864384440 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.cs @@ -44,6 +44,11 @@ public static IEnumerable Select( if (source is IList ilist) { + if (IsSizeOptimized) + { + return new SizeOptIListSelectIterator(ilist, selector); + } + if (source is TSource[] array) { if (array.Length == 0) From 18304001ba5000fd0e9f53c4eae3d093a18c126a Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 9 Aug 2025 16:11:15 -0700 Subject: [PATCH 12/15] Size-optimize Where on LINQ --- .../System.Linq/src/System.Linq.csproj | 1 + .../src/System/Linq/Where.SizeOpt.cs | 99 +++++++++++++++++++ .../System.Linq/src/System/Linq/Where.cs | 29 ++++-- 3 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs diff --git a/src/libraries/System.Linq/src/System.Linq.csproj b/src/libraries/System.Linq/src/System.Linq.csproj index baf71793f4dae9..f032db1a303ee1 100644 --- a/src/libraries/System.Linq/src/System.Linq.csproj +++ b/src/libraries/System.Linq/src/System.Linq.csproj @@ -82,6 +82,7 @@ + diff --git a/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs new file mode 100644 index 00000000000000..4024a1c550f295 --- /dev/null +++ b/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Linq +{ + public static partial class Enumerable + { + private sealed partial class SizeOptIListWhereIterator : Iterator + { + private readonly IList _source; + private readonly Func _predicate; + + public SizeOptIListWhereIterator(IList source, Func predicate) + { + Debug.Assert(source is not null && source.Count > 0); + Debug.Assert(predicate is not null); + _source = source; + _predicate = predicate; + } + + private protected override Iterator Clone() => + new SizeOptIListWhereIterator(_source, _predicate); + + public override bool MoveNext() + { + int index = _state - 1; + IList source = _source; + + while ((uint)index < (uint)source.Count) + { + TSource item = source[index]; + index = _state++; + if (_predicate(item)) + { + _current = item; + return true; + } + } + + Dispose(); + return false; + } + + public override IEnumerable Where(Func predicate) => + new SizeOptIListWhereIterator(_source, Utilities.CombinePredicates(_predicate, predicate)); + + public override TSource[] ToArray() + { + var array = new TSource[_source.Count]; + int count = 0; + + foreach (TSource item in _source) + { + if (_predicate(item)) + { + array[count++] = item; + } + } + + Array.Resize(ref array, count); + return array; + } + + public override List ToList() + { + var list = new List(_source.Count); + foreach (TSource item in _source) + { + if (_predicate(item)) + { + list.Add(item); + } + } + return list; + } + + public override int GetCount(bool onlyIfCheap) + { + if (onlyIfCheap) + { + return -1; + } + + int count = 0; + foreach (TSource item in _source) + { + if (_predicate(item)) + { + checked { count++; } + } + } + return count; + } + } + } +} diff --git a/src/libraries/System.Linq/src/System/Linq/Where.cs b/src/libraries/System.Linq/src/System/Linq/Where.cs index 4371af8299fb2e..b692dba4eab35e 100644 --- a/src/libraries/System.Linq/src/System/Linq/Where.cs +++ b/src/libraries/System.Linq/src/System/Linq/Where.cs @@ -26,19 +26,30 @@ public static IEnumerable Where(this IEnumerable sour return iterator.Where(predicate); } - if (source is TSource[] array) + // Only use IList when size-optimizing (no array or List specializations). + if (IsSizeOptimized) { - if (array.Length == 0) + if (source is IList sourceList) { - return []; + return new SizeOptIListWhereIterator(sourceList, predicate); } - - return new ArrayWhereIterator(array, predicate); } - - if (source is List list) + else { - return new ListWhereIterator(list, predicate); + if (source is TSource[] array) + { + if (array.Length == 0) + { + return []; + } + + return new ArrayWhereIterator(array, predicate); + } + + if (source is List list) + { + return new ListWhereIterator(list, predicate); + } } return new IEnumerableWhereIterator(source, predicate); @@ -143,7 +154,7 @@ public override IEnumerable Select(Func sele new IEnumerableWhereSelectIterator(_source, _predicate, selector); public override IEnumerable Where(Func predicate) => - new IEnumerableWhereIterator(_source, CombinePredicates(_predicate, predicate)); + new IEnumerableWhereIterator(_source, Utilities.CombinePredicates(_predicate, predicate)); } /// From b8f31bd55bb976ce2fdf44f070fc0ea37a8bdda8 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 12 Aug 2025 23:41:14 -0700 Subject: [PATCH 13/15] Use same implementation as List for IList in Where.SizeOpt --- .../src/System/Linq/Where.SizeOpt.cs | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs index 4024a1c550f295..8e49701a11efcb 100644 --- a/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs @@ -12,6 +12,7 @@ private sealed partial class SizeOptIListWhereIterator : Iterator _source; private readonly Func _predicate; + private IEnumerator? _enumerator; public SizeOptIListWhereIterator(IList source, Func predicate) { @@ -26,21 +27,27 @@ private protected override Iterator Clone() => public override bool MoveNext() { - int index = _state - 1; - IList source = _source; - - while ((uint)index < (uint)source.Count) + switch (_state) { - TSource item = source[index]; - index = _state++; - if (_predicate(item)) - { - _current = item; - return true; - } + case 1: + _enumerator = _source.GetEnumerator(); + _state = 2; + goto case 2; + case 2: + while (_enumerator!.MoveNext()) + { + TSource item = _enumerator.Current; + if (_predicate(item)) + { + _current = item; + return true; + } + } + + Dispose(); + break; } - Dispose(); return false; } @@ -49,32 +56,40 @@ public override IEnumerable Where(Func predicate) => public override TSource[] ToArray() { - var array = new TSource[_source.Count]; - int count = 0; + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); foreach (TSource item in _source) { if (_predicate(item)) { - array[count++] = item; + builder.Add(item); } } - Array.Resize(ref array, count); - return array; + TSource[] result = builder.ToArray(); + builder.Dispose(); + + return result; } public override List ToList() { - var list = new List(_source.Count); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + foreach (TSource item in _source) { if (_predicate(item)) { - list.Add(item); + builder.Add(item); } } - return list; + + List result = builder.ToList(); + builder.Dispose(); + + return result; } public override int GetCount(bool onlyIfCheap) From 2b18c60c01cc2651f9bf6b2e11e64e03b6e044b6 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 13 Aug 2025 16:09:42 -0700 Subject: [PATCH 14/15] Update test baselines --- .../src/System/Linq/Select.SizeOpt.cs | 24 +++++++++++++------ src/libraries/System.Linq/tests/CountTests.cs | 13 +++++----- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs index d74d300c512719..bf476355511abd 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.SizeOpt.cs @@ -12,6 +12,8 @@ public static partial class Enumerable private sealed class SizeOptIListSelectIterator(IList _source, Func _selector) : Iterator { + private IEnumerator? _enumerator; + public override int GetCount(bool onlyIfCheap) { // In case someone uses Count() to force evaluation of @@ -50,16 +52,24 @@ public override Iterator Take(int count) public override bool MoveNext() { - var source = _source; - int index = _state - 1; - if ((uint)index < (uint)source.Count) + switch (_state) { - _state++; - _current = _selector(source[index]); - return true; + case 1: + _enumerator = _source.GetEnumerator(); + _state = 2; + goto case 2; + case 2: + Debug.Assert(_enumerator is not null); + if (_enumerator.MoveNext()) + { + _current = _selector(_enumerator.Current); + return true; + } + + Dispose(); + break; } - Dispose(); return false; } diff --git a/src/libraries/System.Linq/tests/CountTests.cs b/src/libraries/System.Linq/tests/CountTests.cs index e6a35f22e700b9..27b18f77870218 100644 --- a/src/libraries/System.Linq/tests/CountTests.cs +++ b/src/libraries/System.Linq/tests/CountTests.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using Xunit; +using Xunit.Abstractions; namespace System.Linq.Tests { - public class CountTests : EnumerableTests + public class CountTests(ITestOutputHelper output) : EnumerableTests { [Fact] public void SameResultsRepeatCallsIntQuery() @@ -151,6 +152,7 @@ public void NonEnumeratedCount_SupportedEnumerables_ShouldReturnExpectedCount [MemberData(nameof(NonEnumeratedCount_UnsupportedEnumerables))] public void NonEnumeratedCount_UnsupportedEnumerables_ShouldReturnFalse(IEnumerable source) { + output.WriteLine(source.GetType().FullName); Assert.False(source.TryGetNonEnumeratedCount(out int actualCount)); Assert.Equal(0, actualCount); } @@ -180,15 +182,15 @@ public static IEnumerable NonEnumeratedCount_SupportedEnumerables() yield return WrapArgs(100, Enumerable.Range(1, 100)); yield return WrapArgs(80, Enumerable.Repeat(1, 80)); + yield return WrapArgs(20, Enumerable.Range(1, 20).Reverse()); + yield return WrapArgs(20, Enumerable.Range(1, 20).OrderBy(x => -x)); + yield return WrapArgs(20, Enumerable.Range(1, 10).Concat(Enumerable.Range(11, 10))); if (PlatformDetection.IsLinqSpeedOptimized) { yield return WrapArgs(50, Enumerable.Range(1, 50).Select(x => x + 1)); yield return WrapArgs(4, new int[] { 1, 2, 3, 4 }.Select(x => x + 1)); yield return WrapArgs(50, Enumerable.Range(1, 50).Select(x => x + 1).Select(x => x - 1)); - yield return WrapArgs(20, Enumerable.Range(1, 20).Reverse()); - yield return WrapArgs(20, Enumerable.Range(1, 20).OrderBy(x => -x)); - yield return WrapArgs(20, Enumerable.Range(1, 10).Concat(Enumerable.Range(11, 10))); } static object[] WrapArgs(int expectedCount, IEnumerable source) => [expectedCount, source]; @@ -206,9 +208,6 @@ public static IEnumerable NonEnumeratedCount_UnsupportedEnumerables() yield return WrapArgs(Enumerable.Range(1, 50).Select(x => x + 1)); yield return WrapArgs(new int[] { 1, 2, 3, 4 }.Select(x => x + 1)); yield return WrapArgs(Enumerable.Range(1, 50).Select(x => x + 1).Select(x => x - 1)); - yield return WrapArgs(Enumerable.Range(1, 20).Reverse()); - yield return WrapArgs(Enumerable.Range(1, 20).OrderBy(x => -x)); - yield return WrapArgs(Enumerable.Range(1, 10).Concat(Enumerable.Range(11, 10))); } static object[] WrapArgs(IEnumerable source) => [source]; From 9a4aac64ff2c0bde0339d05bc79c6e009c0c4b4c Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 15 Aug 2025 11:52:25 -0700 Subject: [PATCH 15/15] Respond to PR comments --- .../System.Linq/src/System/Linq/Where.cs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Where.cs b/src/libraries/System.Linq/src/System/Linq/Where.cs index b692dba4eab35e..c0028a1c004a22 100644 --- a/src/libraries/System.Linq/src/System/Linq/Where.cs +++ b/src/libraries/System.Linq/src/System/Linq/Where.cs @@ -27,29 +27,24 @@ public static IEnumerable Where(this IEnumerable sour } // Only use IList when size-optimizing (no array or List specializations). - if (IsSizeOptimized) + if (IsSizeOptimized && source is IList sourceList) { - if (source is IList sourceList) - { - return new SizeOptIListWhereIterator(sourceList, predicate); - } + return new SizeOptIListWhereIterator(sourceList, predicate); } - else + + if (source is TSource[] array) { - if (source is TSource[] array) + if (array.Length == 0) { - if (array.Length == 0) - { - return []; - } - - return new ArrayWhereIterator(array, predicate); + return []; } - if (source is List list) - { - return new ListWhereIterator(list, predicate); - } + return new ArrayWhereIterator(array, predicate); + } + + if (source is List list) + { + return new ListWhereIterator(list, predicate); } return new IEnumerableWhereIterator(source, predicate);