Skip to content

Commit 4d49539

Browse files
Add fast path for linq count with predicate (#102884)
* Add fast path for count with predicate * Also use TryGetSpan in Aggregate, Any, All, Contains, First, and Single --------- Co-authored-by: Stephen Toub <[email protected]>
1 parent 810d646 commit 4d49539

File tree

6 files changed

+159
-32
lines changed

6 files changed

+159
-32
lines changed

src/libraries/System.Linq/src/System/Linq/Aggregate.cs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,37 @@ public static TSource Aggregate<TSource>(this IEnumerable<TSource> source, Func<
1919
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.func);
2020
}
2121

22-
using (IEnumerator<TSource> e = source.GetEnumerator())
22+
TSource result;
23+
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
2324
{
25+
if (span.IsEmpty)
26+
{
27+
ThrowHelper.ThrowNoElementsException();
28+
}
29+
30+
result = span[0];
31+
for (int i = 1; i < span.Length; i++)
32+
{
33+
result = func(result, span[i]);
34+
}
35+
}
36+
else
37+
{
38+
using IEnumerator<TSource> e = source.GetEnumerator();
39+
2440
if (!e.MoveNext())
2541
{
2642
ThrowHelper.ThrowNoElementsException();
2743
}
2844

29-
TSource result = e.Current;
45+
result = e.Current;
3046
while (e.MoveNext())
3147
{
3248
result = func(result, e.Current);
3349
}
34-
35-
return result;
3650
}
51+
52+
return result;
3753
}
3854

3955
public static TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func)
@@ -49,9 +65,19 @@ public static TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSour
4965
}
5066

5167
TAccumulate result = seed;
52-
foreach (TSource element in source)
68+
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
5369
{
54-
result = func(result, element);
70+
foreach (TSource element in span)
71+
{
72+
result = func(result, element);
73+
}
74+
}
75+
else
76+
{
77+
foreach (TSource element in source)
78+
{
79+
result = func(result, element);
80+
}
5581
}
5682

5783
return result;
@@ -75,9 +101,19 @@ public static TResult Aggregate<TSource, TAccumulate, TResult>(this IEnumerable<
75101
}
76102

77103
TAccumulate result = seed;
78-
foreach (TSource element in source)
104+
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
79105
{
80-
result = func(result, element);
106+
foreach (TSource element in span)
107+
{
108+
result = func(result, element);
109+
}
110+
}
111+
else
112+
{
113+
foreach (TSource element in source)
114+
{
115+
result = func(result, element);
116+
}
81117
}
82118

83119
return resultSelector(result);

src/libraries/System.Linq/src/System/Linq/AnyAll.cs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,24 @@ public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource,
5555
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
5656
}
5757

58-
foreach (TSource element in source)
58+
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
5959
{
60-
if (predicate(element))
60+
foreach (TSource element in span)
6161
{
62-
return true;
62+
if (predicate(element))
63+
{
64+
return true;
65+
}
66+
}
67+
}
68+
else
69+
{
70+
foreach (TSource element in source)
71+
{
72+
if (predicate(element))
73+
{
74+
return true;
75+
}
6376
}
6477
}
6578

@@ -78,11 +91,24 @@ public static bool All<TSource>(this IEnumerable<TSource> source, Func<TSource,
7891
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
7992
}
8093

81-
foreach (TSource element in source)
94+
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
95+
{
96+
foreach (TSource element in span)
97+
{
98+
if (!predicate(element))
99+
{
100+
return false;
101+
}
102+
}
103+
}
104+
else
82105
{
83-
if (!predicate(element))
106+
foreach (TSource element in source)
84107
{
85-
return false;
108+
if (!predicate(element))
109+
{
110+
return false;
111+
}
86112
}
87113
}
88114

src/libraries/System.Linq/src/System/Linq/Contains.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,28 @@ public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource v
2020

2121
if (comparer is null)
2222
{
23+
// While it's tempting, this must not delegate to ICollection<TSource>.Contains, as the historical semantics
24+
// of a null comparer with this method are to use EqualityComparer<TSource>.Default, and that might differ
25+
// from the semantics encoded in ICollection<TSource>.Contains.
26+
27+
// We don't bother special-casing spans here as explicitly providing a null comparer with a known collection type
28+
// is relatively rare. If you don't care about the comparer, you use the other overload, and while it will delegate
29+
// to this overload with a null comparer, it'll only do so for collections from which we can't extract a span.
30+
// And if you do care about the comparer, you're generally passing in a non-null one.
31+
2332
foreach (TSource element in source)
2433
{
25-
if (EqualityComparer<TSource>.Default.Equals(element, value)) // benefits from devirtualization and likely inlining
34+
if (EqualityComparer<TSource>.Default.Equals(element, value))
35+
{
36+
return true;
37+
}
38+
}
39+
}
40+
else if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
41+
{
42+
foreach (TSource element in span)
43+
{
44+
if (comparer.Equals(element, value))
2645
{
2746
return true;
2847
}

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,26 @@ public static int Count<TSource>(this IEnumerable<TSource> source, Func<TSource,
6060
}
6161

6262
int count = 0;
63-
foreach (TSource element in source)
63+
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
6464
{
65-
checked
65+
foreach (TSource element in span)
6666
{
6767
if (predicate(element))
6868
{
6969
count++;
7070
}
7171
}
7272
}
73+
else
74+
{
75+
foreach (TSource element in source)
76+
{
77+
if (predicate(element))
78+
{
79+
checked { count++; }
80+
}
81+
}
82+
}
7383

7484
return count;
7585
}
@@ -136,15 +146,15 @@ public static long LongCount<TSource>(this IEnumerable<TSource> source)
136146
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
137147
}
138148

149+
// TryGetSpan isn't used here because if it's expected that there are more than int.MaxValue elements,
150+
// the source can't possibly be something from which we can extract a span.
151+
139152
long count = 0;
140153
using (IEnumerator<TSource> e = source.GetEnumerator())
141154
{
142-
checked
155+
while (e.MoveNext())
143156
{
144-
while (e.MoveNext())
145-
{
146-
count++;
147-
}
157+
checked { count++; }
148158
}
149159
}
150160

@@ -163,15 +173,15 @@ public static long LongCount<TSource>(this IEnumerable<TSource> source, Func<TSo
163173
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
164174
}
165175

176+
// TryGetSpan isn't used here because if it's expected that there are more than int.MaxValue elements,
177+
// the source can't possibly be something from which we can extract a span.
178+
166179
long count = 0;
167180
foreach (TSource element in source)
168181
{
169-
checked
182+
if (predicate(element))
170183
{
171-
if (predicate(element))
172-
{
173-
count++;
174-
}
184+
checked { count++; }
175185
}
176186
}
177187

src/libraries/System.Linq/src/System/Linq/First.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,26 @@ public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source,
114114
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
115115
}
116116

117-
foreach (TSource element in source)
117+
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
118118
{
119-
if (predicate(element))
119+
foreach (TSource element in span)
120120
{
121-
found = true;
122-
return element;
121+
if (predicate(element))
122+
{
123+
found = true;
124+
return element;
125+
}
126+
}
127+
}
128+
else
129+
{
130+
foreach (TSource element in source)
131+
{
132+
if (predicate(element))
133+
{
134+
found = true;
135+
return element;
136+
}
123137
}
124138
}
125139

src/libraries/System.Linq/src/System/Linq/Single.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,30 @@ public static TSource SingleOrDefault<TSource>(this IEnumerable<TSource> source,
117117
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
118118
}
119119

120-
using (IEnumerator<TSource> e = source.GetEnumerator())
120+
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
121121
{
122+
for (int i = 0; i < span.Length; i++)
123+
{
124+
TSource result = span[i];
125+
if (predicate(result))
126+
{
127+
for (i++; (uint)i < (uint)span.Length; i++)
128+
{
129+
if (predicate(span[i]))
130+
{
131+
ThrowHelper.ThrowMoreThanOneMatchException();
132+
}
133+
}
134+
135+
found = true;
136+
return result;
137+
}
138+
}
139+
}
140+
else
141+
{
142+
using IEnumerator<TSource> e = source.GetEnumerator();
143+
122144
while (e.MoveNext())
123145
{
124146
TSource result = e.Current;

0 commit comments

Comments
 (0)