Skip to content

Commit 6070e26

Browse files
authored
Special-case empty enumerables in AsyncEnumerable (#112321)
The goal here is to handle known empty inputs in a way that reduces unnecessary allocation, e.g. avoiding an iterator allocation or avoiding an intermediate collection. This does not modify operators where there isn't such overhead.
1 parent d05d6c2 commit 6070e26

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+920
-186
lines changed

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/AggregateBy.cs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSou
4242
ThrowHelper.ThrowIfNull(keySelector);
4343
ThrowHelper.ThrowIfNull(func);
4444

45-
return Impl(source, keySelector, seed, func, keyComparer, default);
45+
return
46+
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, TAccumulate>>() :
47+
Impl(source, keySelector, seed, func, keyComparer, default);
4648

4749
static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
4850
IAsyncEnumerable<TSource> source,
@@ -117,7 +119,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSou
117119
ThrowHelper.ThrowIfNull(keySelector);
118120
ThrowHelper.ThrowIfNull(func);
119121

120-
return Impl(source, keySelector, seed, func, keyComparer, default);
122+
return
123+
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, TAccumulate>>() :
124+
Impl(source, keySelector, seed, func, keyComparer, default);
121125

122126
static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
123127
IAsyncEnumerable<TSource> source,
@@ -188,7 +192,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSou
188192
ThrowHelper.ThrowIfNull(seedSelector);
189193
ThrowHelper.ThrowIfNull(func);
190194

191-
return Impl(source, keySelector, seedSelector, func, keyComparer, default);
195+
return
196+
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, TAccumulate>>() :
197+
Impl(source, keySelector, seedSelector, func, keyComparer, default);
192198

193199
static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
194200
IAsyncEnumerable<TSource> source,
@@ -264,7 +270,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSou
264270
ThrowHelper.ThrowIfNull(seedSelector);
265271
ThrowHelper.ThrowIfNull(func);
266272

267-
return Impl(source, keySelector, seedSelector, func, keyComparer, default);
273+
return
274+
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, TAccumulate>>() :
275+
Impl(source, keySelector, seedSelector, func, keyComparer, default);
268276

269277
static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
270278
IAsyncEnumerable<TSource> source,
@@ -277,28 +285,26 @@ static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
277285
IAsyncEnumerator<TSource> enumerator = source.GetAsyncEnumerator(cancellationToken);
278286
try
279287
{
280-
if (!await enumerator.MoveNextAsync().ConfigureAwait(false))
288+
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
281289
{
282-
yield break;
283-
}
290+
Dictionary<TKey, TAccumulate> dict = new(keyComparer);
284291

285-
Dictionary<TKey, TAccumulate> dict = new(keyComparer);
286-
287-
do
288-
{
289-
TSource value = enumerator.Current;
290-
TKey key = await keySelector(value, cancellationToken).ConfigureAwait(false);
292+
do
293+
{
294+
TSource value = enumerator.Current;
295+
TKey key = await keySelector(value, cancellationToken).ConfigureAwait(false);
291296

292-
dict[key] = await func(
293-
dict.TryGetValue(key, out TAccumulate? acc) ? acc : await seedSelector(key, cancellationToken).ConfigureAwait(false),
294-
value,
295-
cancellationToken).ConfigureAwait(false);
296-
}
297-
while (await enumerator.MoveNextAsync().ConfigureAwait(false));
297+
dict[key] = await func(
298+
dict.TryGetValue(key, out TAccumulate? acc) ? acc : await seedSelector(key, cancellationToken).ConfigureAwait(false),
299+
value,
300+
cancellationToken).ConfigureAwait(false);
301+
}
302+
while (await enumerator.MoveNextAsync().ConfigureAwait(false));
298303

299-
foreach (KeyValuePair<TKey, TAccumulate> countBy in dict)
300-
{
301-
yield return countBy;
304+
foreach (KeyValuePair<TKey, TAccumulate> countBy in dict)
305+
{
306+
yield return countBy;
307+
}
302308
}
303309
}
304310
finally

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Cast.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ public static IAsyncEnumerable<TResult> Cast<TResult>( // satisfies the C# query
2626
{
2727
ThrowHelper.ThrowIfNull(source);
2828

29-
return source is IAsyncEnumerable<TResult> result ?
30-
result :
29+
return
30+
source.IsKnownEmpty() ? Empty<TResult>() :
31+
source as IAsyncEnumerable<TResult> ??
3132
Impl(source, default);
3233

3334
static async IAsyncEnumerable<TResult> Impl(

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Chunk.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ public static IAsyncEnumerable<TSource[]> Chunk<TSource>(
3030
ThrowHelper.ThrowIfNull(source);
3131
ThrowHelper.ThrowIfNegativeOrZero(size);
3232

33-
return Chunk(source, size, default);
33+
return
34+
source.IsKnownEmpty() ? Empty<TSource[]>() :
35+
Chunk(source, size, default);
3436

3537
async static IAsyncEnumerable<TSource[]> Chunk(
3638
IAsyncEnumerable<TSource> source,

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Concat.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ public static IAsyncEnumerable<TSource> Concat<TSource>(
2323
ThrowHelper.ThrowIfNull(first);
2424
ThrowHelper.ThrowIfNull(second);
2525

26-
return Impl(first, second, default);
26+
return
27+
first.IsKnownEmpty() ? second :
28+
second.IsKnownEmpty() ? first :
29+
Impl(first, second, default);
2730

2831
static async IAsyncEnumerable<TSource> Impl(
2932
IAsyncEnumerable<TSource> first,

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/CountBy.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(
2828
ThrowHelper.ThrowIfNull(source);
2929
ThrowHelper.ThrowIfNull(keySelector);
3030

31-
return Impl(source, keySelector, keyComparer, default);
31+
return
32+
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, int>>() :
33+
Impl(source, keySelector, keyComparer, default);
3234

3335
static async IAsyncEnumerable<KeyValuePair<TKey, int>> Impl(
3436
IAsyncEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? keyComparer, [EnumeratorCancellation] CancellationToken cancellationToken)
@@ -83,7 +85,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(
8385
ThrowHelper.ThrowIfNull(source);
8486
ThrowHelper.ThrowIfNull(keySelector);
8587

86-
return Impl(source, keySelector, keyComparer, default);
88+
return
89+
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, int>>() :
90+
Impl(source, keySelector, keyComparer, default);
8791

8892
static async IAsyncEnumerable<KeyValuePair<TKey, int>> Impl(
8993
IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask<TKey>> keySelector, IEqualityComparer<TKey>? keyComparer, [EnumeratorCancellation] CancellationToken cancellationToken)

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Distinct.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public static IAsyncEnumerable<TSource> Distinct<TSource>(
2121
{
2222
ThrowHelper.ThrowIfNull(source);
2323

24-
return Impl(source, comparer, default);
24+
return
25+
source.IsKnownEmpty() ? Empty<TSource>() :
26+
Impl(source, comparer, default);
2527

2628
static async IAsyncEnumerable<TSource> Impl(
2729
IAsyncEnumerable<TSource> source,

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/DistinctBy.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public static IAsyncEnumerable<TSource> DistinctBy<TSource, TKey>(
3232
ThrowHelper.ThrowIfNull(source);
3333
ThrowHelper.ThrowIfNull(keySelector);
3434

35-
return Impl(source, keySelector, comparer, default);
35+
return
36+
source.IsKnownEmpty() ? Empty<TSource>() :
37+
Impl(source, keySelector, comparer, default);
3638

3739
static async IAsyncEnumerable<TSource> Impl(
3840
IAsyncEnumerable<TSource> source,
@@ -86,7 +88,9 @@ public static IAsyncEnumerable<TSource> DistinctBy<TSource, TKey>(
8688
ThrowHelper.ThrowIfNull(source);
8789
ThrowHelper.ThrowIfNull(keySelector);
8890

89-
return Impl(source, keySelector, comparer, default);
91+
return
92+
source.IsKnownEmpty() ? Empty<TSource>() :
93+
Impl(source, keySelector, comparer, default);
9094

9195
static async IAsyncEnumerable<TSource> Impl(
9296
IAsyncEnumerable<TSource> source,

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Empty.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ public static partial class AsyncEnumerable
1616
/// <returns>An empty <see cref="IAsyncEnumerable{T}"/> whose type argument is <typeparamref name="TResult"/>.</returns>
1717
public static IAsyncEnumerable<TResult> Empty<TResult>() => EmptyAsyncEnumerable<TResult>.Instance;
1818

19-
private sealed class EmptyAsyncEnumerable<TResult> : IAsyncEnumerable<TResult>, IAsyncEnumerator<TResult>
19+
/// <summary>Determines whether <paramref name="source"/> is known to be an always-empty enumerable.</summary>
20+
private static bool IsKnownEmpty<TResult>(this IAsyncEnumerable<TResult> source) =>
21+
ReferenceEquals(source, EmptyAsyncEnumerable<TResult>.Instance);
22+
23+
private sealed class EmptyAsyncEnumerable<TResult> :
24+
IAsyncEnumerable<TResult>, IAsyncEnumerator<TResult>, IOrderedAsyncEnumerable<TResult>
2025
{
21-
public static EmptyAsyncEnumerable<TResult> Instance { get; } = new EmptyAsyncEnumerable<TResult>();
26+
public static readonly EmptyAsyncEnumerable<TResult> Instance = new();
2227

2328
public IAsyncEnumerator<TResult> GetAsyncEnumerator(CancellationToken cancellationToken = default) => this;
2429

@@ -27,6 +32,18 @@ private sealed class EmptyAsyncEnumerable<TResult> : IAsyncEnumerable<TResult>,
2732
public TResult Current => default!;
2833

2934
public ValueTask DisposeAsync() => default;
35+
36+
public IOrderedAsyncEnumerable<TResult> CreateOrderedAsyncEnumerable<TKey>(Func<TResult, TKey> keySelector, IComparer<TKey>? comparer, bool descending)
37+
{
38+
ThrowHelper.ThrowIfNull(keySelector);
39+
return this;
40+
}
41+
42+
public IOrderedAsyncEnumerable<TResult> CreateOrderedAsyncEnumerable<TKey>(Func<TResult, CancellationToken, ValueTask<TKey>> keySelector, IComparer<TKey>? comparer, bool descending)
43+
{
44+
ThrowHelper.ThrowIfNull(keySelector);
45+
return this;
46+
}
3047
}
3148
}
3249
}

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Except.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,44 @@ public static IAsyncEnumerable<TSource> Except<TSource>(
2626
ThrowHelper.ThrowIfNull(first);
2727
ThrowHelper.ThrowIfNull(second);
2828

29-
return Impl(first, second, comparer, default);
29+
return
30+
first.IsKnownEmpty() ? Empty<TSource>() :
31+
Impl(first, second, comparer, default);
3032

3133
async static IAsyncEnumerable<TSource> Impl(
3234
IAsyncEnumerable<TSource> first,
3335
IAsyncEnumerable<TSource> second,
3436
IEqualityComparer<TSource>? comparer,
3537
[EnumeratorCancellation] CancellationToken cancellationToken)
3638
{
37-
HashSet<TSource> set = new(comparer);
38-
39-
await foreach (TSource element in second.WithCancellation(cancellationToken).ConfigureAwait(false))
39+
IAsyncEnumerator<TSource> firstEnumerator = first.GetAsyncEnumerator(cancellationToken);
40+
try
4041
{
41-
set.Add(element);
42-
}
42+
if (!await firstEnumerator.MoveNextAsync().ConfigureAwait(false))
43+
{
44+
yield break;
45+
}
4346

44-
await foreach (TSource element in first.WithCancellation(cancellationToken).ConfigureAwait(false))
45-
{
46-
if (set.Add(element))
47+
HashSet<TSource> set = new(comparer);
48+
49+
await foreach (TSource element in second.WithCancellation(cancellationToken).ConfigureAwait(false))
4750
{
48-
yield return element;
51+
set.Add(element);
4952
}
53+
54+
do
55+
{
56+
TSource firstElement = firstEnumerator.Current;
57+
if (set.Add(firstElement))
58+
{
59+
yield return firstElement;
60+
}
61+
}
62+
while (await firstEnumerator.MoveNextAsync().ConfigureAwait(false));
63+
}
64+
finally
65+
{
66+
await firstEnumerator.DisposeAsync().ConfigureAwait(false);
5067
}
5168
}
5269
}

src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/ExceptBy.cs

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ public static IAsyncEnumerable<TSource> ExceptBy<TSource, TKey>(
3333
ThrowHelper.ThrowIfNull(second);
3434
ThrowHelper.ThrowIfNull(keySelector);
3535

36-
return Impl(first, second, keySelector, comparer, default);
36+
return
37+
first.IsKnownEmpty() ? Empty<TSource>() :
38+
Impl(first, second, keySelector, comparer, default);
3739

3840
static async IAsyncEnumerable<TSource> Impl(
3941
IAsyncEnumerable<TSource> first,
@@ -42,19 +44,34 @@ static async IAsyncEnumerable<TSource> Impl(
4244
IEqualityComparer<TKey>? comparer,
4345
[EnumeratorCancellation] CancellationToken cancellationToken)
4446
{
45-
HashSet<TKey> set = new(comparer);
46-
47-
await foreach (TKey key in second.WithCancellation(cancellationToken).ConfigureAwait(false))
47+
IAsyncEnumerator<TSource> firstEnumerator = first.GetAsyncEnumerator(cancellationToken);
48+
try
4849
{
49-
set.Add(key);
50-
}
50+
if (!await firstEnumerator.MoveNextAsync().ConfigureAwait(false))
51+
{
52+
yield break;
53+
}
5154

52-
await foreach (TSource element in first.WithCancellation(cancellationToken).ConfigureAwait(false))
53-
{
54-
if (set.Add(keySelector(element)))
55+
HashSet<TKey> set = new(comparer);
56+
57+
await foreach (TKey key in second.WithCancellation(cancellationToken).ConfigureAwait(false))
5558
{
56-
yield return element;
59+
set.Add(key);
5760
}
61+
62+
do
63+
{
64+
TSource firstElement = firstEnumerator.Current;
65+
if (set.Add(keySelector(firstElement)))
66+
{
67+
yield return firstElement;
68+
}
69+
}
70+
while (await firstEnumerator.MoveNextAsync().ConfigureAwait(false));
71+
}
72+
finally
73+
{
74+
await firstEnumerator.DisposeAsync().ConfigureAwait(false);
5875
}
5976
}
6077
}
@@ -82,7 +99,9 @@ public static IAsyncEnumerable<TSource> ExceptBy<TSource, TKey>(
8299
ThrowHelper.ThrowIfNull(second);
83100
ThrowHelper.ThrowIfNull(keySelector);
84101

85-
return Impl(first, second, keySelector, comparer, default);
102+
return
103+
first.IsKnownEmpty() ? Empty<TSource>() :
104+
Impl(first, second, keySelector, comparer, default);
86105

87106
static async IAsyncEnumerable<TSource> Impl(
88107
IAsyncEnumerable<TSource> first,
@@ -91,19 +110,34 @@ static async IAsyncEnumerable<TSource> Impl(
91110
IEqualityComparer<TKey>? comparer,
92111
[EnumeratorCancellation] CancellationToken cancellationToken)
93112
{
94-
HashSet<TKey> set = new(comparer);
95-
96-
await foreach (TKey key in second.WithCancellation(cancellationToken).ConfigureAwait(false))
113+
IAsyncEnumerator<TSource> firstEnumerator = first.GetAsyncEnumerator(cancellationToken);
114+
try
97115
{
98-
set.Add(key);
99-
}
116+
if (!await firstEnumerator.MoveNextAsync().ConfigureAwait(false))
117+
{
118+
yield break;
119+
}
100120

101-
await foreach (TSource element in first.WithCancellation(cancellationToken).ConfigureAwait(false))
102-
{
103-
if (set.Add(await keySelector(element, cancellationToken).ConfigureAwait(false)))
121+
HashSet<TKey> set = new(comparer);
122+
123+
await foreach (TKey key in second.WithCancellation(cancellationToken).ConfigureAwait(false))
104124
{
105-
yield return element;
125+
set.Add(key);
106126
}
127+
128+
do
129+
{
130+
TSource firstElement = firstEnumerator.Current;
131+
if (set.Add(await keySelector(firstElement, cancellationToken).ConfigureAwait(false)))
132+
{
133+
yield return firstElement;
134+
}
135+
}
136+
while (await firstEnumerator.MoveNextAsync().ConfigureAwait(false));
137+
}
138+
finally
139+
{
140+
await firstEnumerator.DisposeAsync().ConfigureAwait(false);
107141
}
108142
}
109143
}

0 commit comments

Comments
 (0)