Skip to content

Commit 58d1c2e

Browse files
authored
Introduce size-optimized IListSelect iterator (#118156)
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.
1 parent c0038ce commit 58d1c2e

19 files changed

+285
-97
lines changed

src/libraries/System.Linq/src/System.Linq.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Compile Include="System\Linq\RightJoin.cs" />
6363
<Compile Include="System\Linq\SegmentedArrayBuilder.cs" />
6464
<Compile Include="System\Linq\Select.cs" />
65+
<Compile Include="System\Linq\Select.SizeOpt.cs" />
6566
<Compile Include="System\Linq\Select.SpeedOpt.cs" />
6667
<Compile Include="System\Linq\SelectMany.cs" />
6768
<Compile Include="System\Linq\SelectMany.SpeedOpt.cs" />
@@ -70,19 +71,18 @@
7071
<Compile Include="System\Linq\Single.cs" />
7172
<Compile Include="System\Linq\SingleLinkedNode.cs" />
7273
<Compile Include="System\Linq\Skip.cs" />
73-
<Compile Include="System\Linq\Skip.SizeOpt.cs" />
7474
<Compile Include="System\Linq\Skip.SpeedOpt.cs" />
7575
<Compile Include="System\Linq\SkipTake.SpeedOpt.cs" />
7676
<Compile Include="System\Linq\Sum.cs" />
7777
<Compile Include="System\Linq\Take.cs" />
78-
<Compile Include="System\Linq\Take.SizeOpt.cs" />
7978
<Compile Include="System\Linq\Take.SpeedOpt.cs" />
8079
<Compile Include="System\Linq\ThrowHelper.cs" />
8180
<Compile Include="System\Linq\ToCollection.cs" />
8281
<Compile Include="System\Linq\Union.cs" />
8382
<Compile Include="System\Linq\Union.SpeedOpt.cs" />
8483
<Compile Include="System\Linq\Utilities.cs" />
8584
<Compile Include="System\Linq\Where.cs" />
85+
<Compile Include="System\Linq\Where.SizeOpt.cs" />
8686
<Compile Include="System\Linq\Where.SpeedOpt.cs" />
8787
<Compile Include="System\Linq\Zip.cs" />
8888
</ItemGroup>

src/libraries/System.Linq/src/System/Linq/Count.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static int Count<TSource>(this IEnumerable<TSource> source)
2020
return collectionoft.Count;
2121
}
2222

23-
if (!IsSizeOptimized && source is Iterator<TSource> iterator)
23+
if (source is Iterator<TSource> iterator)
2424
{
2525
return iterator.GetCount(onlyIfCheap: false);
2626
}
@@ -113,7 +113,7 @@ public static bool TryGetNonEnumeratedCount<TSource>(this IEnumerable<TSource> s
113113
return true;
114114
}
115115

116-
if (!IsSizeOptimized && source is Iterator<TSource> iterator)
116+
if (source is Iterator<TSource> iterator)
117117
{
118118
int c = iterator.GetCount(onlyIfCheap: true);
119119
if (c >= 0)

src/libraries/System.Linq/src/System/Linq/ElementAt.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int i
2323

2424
bool found;
2525
TSource? element =
26-
!IsSizeOptimized && source is Iterator<TSource> iterator ? iterator.TryGetElementAt(index, out found) :
26+
source is Iterator<TSource> iterator ? iterator.TryGetElementAt(index, out found) :
2727
TryGetElementAtNonIterator(source, index, out found);
2828

2929
if (!found)
@@ -121,7 +121,7 @@ public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, Index
121121
}
122122

123123
return
124-
!IsSizeOptimized && source is Iterator<TSource> iterator ? iterator.TryGetElementAt(index, out found) :
124+
source is Iterator<TSource> iterator ? iterator.TryGetElementAt(index, out found) :
125125
TryGetElementAtNonIterator(source, index, out found);
126126
}
127127

src/libraries/System.Linq/src/System/Linq/Last.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public static TSource LastOrDefault<TSource>(this IEnumerable<TSource> source, F
6969
}
7070

7171
return
72-
!IsSizeOptimized && source is Iterator<TSource> iterator ? iterator.TryGetLast(out found) :
72+
source is Iterator<TSource> iterator ? iterator.TryGetLast(out found) :
7373
TryGetLastNonIterator(source, out found);
7474
}
7575

src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public override IEnumerable<TResult2> Select<TResult2>(Func<TResult, TResult2> s
167167
// they're covariant. It's not worthwhile checking for List<T> to use the ListWhereSelectIterator
168168
// because List<> is not covariant.
169169
Func<object, bool> isTResult = static o => o is TResult;
170-
return objectSource is object[] array ?
170+
return !IsSizeOptimized && objectSource is object[] array ?
171171
new ArrayWhereSelectIterator<object, TResult2>(array, isTResult, localSelector) :
172172
new IEnumerableWhereSelectIterator<object, TResult2>(objectSource, isTResult, localSelector);
173173
}
@@ -177,7 +177,11 @@ public override IEnumerable<TResult2> Select<TResult2>(Func<TResult, TResult2> s
177177

178178
public override bool Contains(TResult value)
179179
{
180-
if (!typeof(TResult).IsValueType && // don't box TResult
180+
// Avoid checking for IList when size-optimized because it keeps IList
181+
// implementations which may otherwise be trimmed. Since List<T> implements
182+
// IList and List<T> is popular, this could potentially be a lot of code.
183+
if (!IsSizeOptimized &&
184+
!typeof(TResult).IsValueType && // don't box TResult
181185
_source is IList list)
182186
{
183187
return list.Contains(value);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
8+
namespace System.Linq
9+
{
10+
public static partial class Enumerable
11+
{
12+
private sealed class SizeOptIListSelectIterator<TSource, TResult>(IList<TSource> _source, Func<TSource, TResult> _selector)
13+
: Iterator<TResult>
14+
{
15+
private IEnumerator<TSource>? _enumerator;
16+
17+
public override int GetCount(bool onlyIfCheap)
18+
{
19+
// In case someone uses Count() to force evaluation of
20+
// the selector, run it provided `onlyIfCheap` is false.
21+
22+
if (onlyIfCheap)
23+
{
24+
return -1;
25+
}
26+
27+
int count = 0;
28+
29+
foreach (TSource item in _source)
30+
{
31+
_selector(item);
32+
checked
33+
{
34+
count++;
35+
}
36+
}
37+
38+
return count;
39+
}
40+
41+
public override Iterator<TResult> Skip(int count)
42+
{
43+
Debug.Assert(count > 0);
44+
return new IListSkipTakeSelectIterator<TSource, TResult>(_source, _selector, count, int.MaxValue);
45+
}
46+
47+
public override Iterator<TResult> Take(int count)
48+
{
49+
Debug.Assert(count > 0);
50+
return new IListSkipTakeSelectIterator<TSource, TResult>(_source, _selector, 0, count - 1);
51+
}
52+
53+
public override bool MoveNext()
54+
{
55+
switch (_state)
56+
{
57+
case 1:
58+
_enumerator = _source.GetEnumerator();
59+
_state = 2;
60+
goto case 2;
61+
case 2:
62+
Debug.Assert(_enumerator is not null);
63+
if (_enumerator.MoveNext())
64+
{
65+
_current = _selector(_enumerator.Current);
66+
return true;
67+
}
68+
69+
Dispose();
70+
break;
71+
}
72+
73+
return false;
74+
}
75+
76+
public override TResult[] ToArray()
77+
{
78+
TResult[] array = new TResult[_source.Count];
79+
for (int i = 0; i < array.Length; i++)
80+
{
81+
array[i] = _selector(_source[i]);
82+
}
83+
return array;
84+
}
85+
86+
public override List<TResult> ToList()
87+
{
88+
List<TResult> list = new List<TResult>(_source.Count);
89+
for (int i = 0; i < list.Count; i++)
90+
{
91+
list.Add(_selector(_source[i]));
92+
}
93+
return list;
94+
}
95+
96+
private protected override Iterator<TResult> Clone()
97+
=> new SizeOptIListSelectIterator<TSource, TResult>(_source, _selector);
98+
}
99+
}
100+
}

src/libraries/System.Linq/src/System/Linq/Select.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public static IEnumerable<TResult> Select<TSource, TResult>(
3232
// don't need more code, just more data structures describing the new types).
3333
if (IsSizeOptimized && typeof(TResult).IsValueType)
3434
{
35-
return new IEnumerableSelectIterator<TSource, TResult>(iterator, selector);
35+
return source is IList<TSource> il
36+
? new SizeOptIListSelectIterator<TSource, TResult>(il, selector)
37+
: new IEnumerableSelectIterator<TSource, TResult>(iterator, selector);
3638
}
3739
else
3840
{
@@ -42,6 +44,11 @@ public static IEnumerable<TResult> Select<TSource, TResult>(
4244

4345
if (source is IList<TSource> ilist)
4446
{
47+
if (IsSizeOptimized)
48+
{
49+
return new SizeOptIListSelectIterator<TSource, TResult>(ilist, selector);
50+
}
51+
4552
if (source is TSource[] array)
4653
{
4754
if (array.Length == 0)

src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/libraries/System.Linq/src/System/Linq/Skip.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ public static IEnumerable<TSource> Skip<TSource>(this IEnumerable<TSource> sourc
3030

3131
count = 0;
3232
}
33-
else if (!IsSizeOptimized && source is Iterator<TSource> iterator)
33+
else if (source is Iterator<TSource> iterator)
3434
{
3535
return iterator.Skip(count) ?? Empty<TSource>();
3636
}
3737

38-
return IsSizeOptimized ? SizeOptimizedSkipIterator(source, count) : SpeedOptimizedSkipIterator(source, count);
38+
return SpeedOptimizedSkipIterator(source, count);
3939
}
4040

4141
public static IEnumerable<TSource> SkipWhile<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)

src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs

Lines changed: 0 additions & 47 deletions
This file was deleted.

0 commit comments

Comments
 (0)