Skip to content

Commit 8ffc96c

Browse files
authored
Revamp empty special-casing in LINQ (dotnet#96602)
* Revamp empty special-casing in LINQ Enumerable.Empty used to return Array.Empty. Towards the beginning of .NET Core, LINQ was imbued with an internal "partition" concept for flowing more information around between operators, and as part of that, Empty was changed to return a singleton instance of a specialized partition implementation. The upside of this was that methods typed to return IPartition could return the same singleton as Empty. There are multiple downsides, however. For one, the whole IPartition concept is only built into a "speed-optimized" build of LINQ; builds that care more about size (e.g. browser) end up not having it, and thus Empty there ends up being Array.Empty, such that a different type ends up being returned based on the build, which is not ideal. Further, any paths that check for empty now effectively have two things to check for: the empty partition or an empty array, making those checks more expensive, if they're even done at all, or in some cases missing out on possible optimization. This is more pronounced today, now that `[]` with collection expressions will produce Array.Empty, and it'd be really nice if there wasn't a difference between Enumerable.Empty and `[]` assigned to `IEnumerable<T>`. This change puts Enumerable.Empty back to always being Array.Empty. The internal IPartition-based APIs that drove us to need the EmptyPartition are changed to just use null as an indication of empty. Places we were already checking for `is EmptyPartition` are changed to check for an empty array (if they weren't already), and other APIs that weren't checking at all now have a check if it makes sense to do so (I audited all of the APIs, and didn't include checks in ones where it could meaningfully affect semantics, e.g. a fast path that might cause us not to get an enumerator from a secondary enumerable input). * Rename IsImmutableEmpty to IsEmptyArray per PR feedback * Remove bogus PLINQ tests The tests were validating the underlying type of the operator returned for Concat, which is not material. * Fix Skip special-casing The check needs to be moved to before the count <= 0 check... otherwise calling Skip on an empty array with a count of 0 would still allocate an iterator.
1 parent 1df91d3 commit 8ffc96c

40 files changed

+350
-487
lines changed

src/libraries/System.Linq.Parallel/tests/QueryOperators/AsEnumerableTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public static void AsEnumerable_LinqBinding(Labeled<ParallelQuery<int>> labeled,
5757
Assert.IsNotType<ParallelQuery<int>>(enumerable.Cast<int>());
5858
Assert.True(enumerable.Cast<int>() is ParallelQuery<int>);
5959

60-
Assert.False(enumerable.Concat(Enumerable.Range(0, count)) is ParallelQuery<int>);
6160
Assert.False(enumerable.DefaultIfEmpty() is ParallelQuery<int>);
6261
Assert.False(enumerable.Distinct() is ParallelQuery<int>);
6362
Assert.False(enumerable.Except(Enumerable.Range(0, count)) is ParallelQuery<int>);

src/libraries/System.Linq.Parallel/tests/QueryOperators/AsSequentialTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public static void AsSequential_LinqBinding(Labeled<ParallelQuery<int>> labeled,
5757
Assert.IsNotType<ParallelQuery<int>>(seq.Cast<int>());
5858
Assert.True(seq.Cast<int>() is ParallelQuery<int>);
5959

60-
Assert.False(seq.Concat(Enumerable.Range(0, count)) is ParallelQuery<int>);
6160
Assert.False(seq.DefaultIfEmpty() is ParallelQuery<int>);
6261
Assert.False(seq.Distinct() is ParallelQuery<int>);
6362
Assert.False(seq.Except(Enumerable.Range(0, count)) is ParallelQuery<int>);

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
</PropertyGroup>
1313

1414
<ItemGroup Condition="'$(OptimizeForSize)' == true">
15-
<Compile Include="System\Linq\Enumerable.SizeOpt.cs" />
1615
<Compile Include="System\Linq\Skip.SizeOpt.cs" />
1716
<Compile Include="System\Linq\Take.SizeOpt.cs" />
1817
<Compile Include="$(CommonPath)\System\Collections\Generic\LargeArrayBuilder.SizeOpt.cs"
@@ -24,7 +23,6 @@
2423
<Compile Include="System\Linq\Concat.SpeedOpt.cs" />
2524
<Compile Include="System\Linq\DefaultIfEmpty.SpeedOpt.cs" />
2625
<Compile Include="System\Linq\Distinct.SpeedOpt.cs" />
27-
<Compile Include="System\Linq\Enumerable.SpeedOpt.cs" />
2826
<Compile Include="System\Linq\Grouping.SpeedOpt.cs" />
2927
<Compile Include="System\Linq\Lookup.SpeedOpt.cs" />
3028
<Compile Include="System\Linq\OrderedEnumerable.SpeedOpt.cs" />

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public static IEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSource,
2828
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.func);
2929
}
3030

31+
if (IsEmptyArray(source))
32+
{
33+
return [];
34+
}
35+
3136
return AggregateByIterator(source, keySelector, seed, func, keyComparer);
3237
}
3338

@@ -55,6 +60,11 @@ public static IEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSource,
5560
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.func);
5661
}
5762

63+
if (IsEmptyArray(source))
64+
{
65+
return [];
66+
}
67+
5868
return AggregateByIterator(source, keySelector, seedSelector, func, keyComparer);
5969
}
6070

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
using System.Collections.Generic;
55
using System.Diagnostics;
6-
using System.Diagnostics.CodeAnalysis;
7-
using System.Runtime.CompilerServices;
86

97
namespace System.Linq
108
{
@@ -47,6 +45,11 @@ public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> so
4745
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.size);
4846
}
4947

48+
if (IsEmptyArray(source))
49+
{
50+
return [];
51+
}
52+
5053
return ChunkIterator(source, size);
5154
}
5255

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ private TSource[] PreallocatingToArray()
150150

151151
if (count == 0)
152152
{
153-
return Array.Empty<TSource>();
153+
return [];
154154
}
155155

156156
var array = new TSource[count];

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ public static IEnumerable<TSource> Concat<TSource>(this IEnumerable<TSource> fir
2020
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.second);
2121
}
2222

23+
if (IsEmptyArray(first))
24+
{
25+
return second;
26+
}
27+
28+
if (IsEmptyArray(second))
29+
{
30+
return first;
31+
}
32+
2333
return first is ConcatIterator<TSource> firstConcat
2434
? firstConcat.Concat(second)
2535
: new Concat2Iterator<TSource>(first, second);

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public static IEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(this I
1919
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.keySelector);
2020
}
2121

22+
if (IsEmptyArray(source))
23+
{
24+
return [];
25+
}
26+
2227
return CountByIterator(source, keySelector, keyComparer);
2328
}
2429

src/libraries/System.Linq/src/System/Linq/DebugView.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,7 @@ public object?[] Items
7676
{
7777
get
7878
{
79-
var tempList = new List<object?>();
80-
foreach (object? item in _enumerable)
81-
{
82-
tempList.Add(item);
83-
}
79+
List<object?> tempList = [.. _enumerable];
8480

8581
if (tempList.Count == 0)
8682
{
@@ -114,10 +110,10 @@ public SystemLinq_GroupingDebugView(Grouping<TKey, TElement> grouping)
114110

115111
internal sealed class SystemLinq_LookupDebugView<TKey, TElement>
116112
{
117-
private readonly Lookup<TKey, TElement> _lookup;
113+
private readonly ILookup<TKey, TElement> _lookup;
118114
private IGrouping<TKey, TElement>[]? _cachedGroupings;
119115

120-
public SystemLinq_LookupDebugView(Lookup<TKey, TElement> lookup)
116+
public SystemLinq_LookupDebugView(ILookup<TKey, TElement> lookup)
121117
{
122118
_lookup = lookup;
123119
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ private sealed partial class DefaultIfEmptyIterator<TSource> : IIListProvider<TS
1313
public TSource[] ToArray()
1414
{
1515
TSource[] array = _source.ToArray();
16-
return array.Length == 0 ? new[] { _default } : array;
16+
return array.Length == 0 ? [_default] : array;
1717
}
1818

1919
public List<TSource> ToList()

0 commit comments

Comments
 (0)