From 5e13d8f9669d03cc69a6402ecdf68e197ac38972 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 28 Jul 2025 15:59:49 -0700 Subject: [PATCH 01/16] 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 beedde840f4959961edbd8dddaca0ad2a5444f2b Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 29 Jul 2025 08:49:26 -0700 Subject: [PATCH 02/16] 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 4066682d4105de4d8bde521ea7a86d7856288e02 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 29 Jul 2025 11:03:28 -0700 Subject: [PATCH 03/16] 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 81c32a77453720269dadf71345ff0d458c1dc944 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 29 Jul 2025 13:33:03 -0700 Subject: [PATCH 04/16] 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 d7513a418ba6d701c22e799c6c9deb6ed18c0bc3 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 30 Jul 2025 00:07:36 -0700 Subject: [PATCH 05/16] 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 c66f2d795266cdf09ae29abca015e0e567c54ea1 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 30 Jul 2025 14:21:57 -0700 Subject: [PATCH 06/16] 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 8a814ca2cee110bb42e2ae6212af21b2e6d5a724 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 1 Aug 2025 01:29:55 -0700 Subject: [PATCH 07/16] 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 16404667e68209332430c93677eaed0e64833002 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 1 Aug 2025 10:37:54 -0700 Subject: [PATCH 08/16] 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 190dc8a2230baa05a9ffb31972e01290d12205a3 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 1 Aug 2025 16:17:39 -0700 Subject: [PATCH 09/16] 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 da8efe5407d1a0..fdf07d02406ad4 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -574,6 +574,7 @@ + From d543b503a665ee095406708e113de38503ebfc79 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 8 Aug 2025 16:29:14 -0700 Subject: [PATCH 10/16] 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 29dd183609449d112822600e2f43e30024cc8897 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 9 Aug 2025 15:27:33 -0700 Subject: [PATCH 11/16] 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 c6d904aa5d5c90bca7f50791201876cf78175aa7 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 9 Aug 2025 16:11:15 -0700 Subject: [PATCH 12/16] 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 b972098307b6ad7374ea988472334c5f9decfb68 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 12 Aug 2025 23:41:14 -0700 Subject: [PATCH 13/16] 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 c78f463b08c5629f37d7eb708b0dc1c122993a05 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 13 Aug 2025 16:09:42 -0700 Subject: [PATCH 14/16] 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 cb6a6b6f0c1bc206e2ad390d56894b0ab1a48bfd Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 15 Aug 2025 11:52:25 -0700 Subject: [PATCH 15/16] 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); From 12cdccd0506353d867921cb532e626d3d0ef6e13 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 18 Aug 2025 13:15:16 -0700 Subject: [PATCH 16/16] Remove incorrect assert clause (#118828) --- src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8e49701a11efcb..ebb94853261d81 100644 --- a/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Where.SizeOpt.cs @@ -16,7 +16,7 @@ private sealed partial class SizeOptIListWhereIterator : Iterator source, Func predicate) { - Debug.Assert(source is not null && source.Count > 0); + Debug.Assert(source is not null); Debug.Assert(predicate is not null); _source = source; _predicate = predicate;