From 73c5b81f1b3118e5ad2fcb77943c516eb97409de Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 5 Jan 2024 20:54:51 -0500 Subject: [PATCH] Replace LINQ's ArrayBuilder, LargeArrayBuilder, and SparseArrayBuilder There's a lot of code involved in these, some of which we special-case to only build into some of the targets, and it's unnecessarily complicated. We can get most of the benefit with a simpler solution, which this attempts to provide. This commit removes those three types and replaces them with a SegmentedArrayBuilder. The changes to ToArray also obviate the need for the old Buffer type. It existed to avoid one array allocation at the end of loading an enumerable, where we'd doubled and doubled and doubled in size, and then eventually have an array with all the data but some extra space. Now that we don't have such an array, we don't need Buffer, and can just the normal Enumerable.ToArray. The one thing the new scheme doesn't handle as well is when there are multiple sources being added (specifically in Concat / SelectMany). Previously, the code used a complicated scheme to reserve space in the output for partial sources known to be ICollections, so it could use ICollection.CopyTo for each consistuent source. But CopyTo doesn't support partial copies, which means we can only use it if there's enough room available in an individual segment. The implementation thus tries to use it, and falls back to normal enumeration if it can't. For larger enumerations where the cost would end up being more apparent, the expectation is we'd quickly grow to a segment size where the subsequent appends are able to fit into the current segment. Hopefully. --- .../Generic/EnumerableHelpers.Linq.cs | 88 ----- .../Generic/LargeArrayBuilder.SizeOpt.cs | 44 --- .../Generic/LargeArrayBuilder.SpeedOpt.cs | 340 ------------------ .../Collections/Generic/LargeArrayBuilder.cs | 62 ---- .../Collections/Generic/SparseArrayBuilder.cs | 224 ------------ .../Common/tests/Common.Tests.csproj | 5 - .../Generic/LargeArrayBuilderTests.cs | 182 ---------- src/libraries/System.Linq/System.Linq.sln | 24 +- .../System.Linq/src/System.Linq.csproj | 16 +- .../src/System/Linq/AppendPrepend.SpeedOpt.cs | 94 +++-- .../System.Linq/src/System/Linq/Average.cs | 10 + .../System.Linq/src/System/Linq/Buffer.cs | 42 --- .../src/System/Linq/Concat.SpeedOpt.cs | 84 +++-- .../System.Linq/src/System/Linq/Enumerable.cs | 12 +- .../System.Linq/src/System/Linq/Max.cs | 10 + .../System.Linq/src/System/Linq/MaxMin.cs | 5 + .../System.Linq/src/System/Linq/Min.cs | 10 + .../System/Linq/OrderedEnumerable.SpeedOpt.cs | 84 +++-- .../src/System/Linq/OrderedEnumerable.cs | 32 +- .../src/System/Linq/Partition.SpeedOpt.cs | 10 +- .../System.Linq/src/System/Linq/Reverse.cs | 6 +- .../src/System/Linq/SegmentedArrayBuilder.cs | 313 ++++++++++++++++ .../src/System/Linq/Select.SpeedOpt.cs | 26 +- .../src/System/Linq/SelectMany.SpeedOpt.cs | 30 +- .../src/System/Linq/SingleLinkedNode.cs | 2 +- .../System.Linq/src/System/Linq/Sum.cs | 5 + .../src/System/Linq/ToCollection.cs | 55 ++- .../System.Linq/src/System/Linq/Utilities.cs | 2 +- .../src/System/Linq/Where.SpeedOpt.cs | 197 ++++------ .../System.Linq/tests/LifecycleTests.cs | 8 +- .../System.Linq/tests/ToArrayTests.cs | 5 +- 31 files changed, 714 insertions(+), 1313 deletions(-) delete mode 100644 src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs delete mode 100644 src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.SizeOpt.cs delete mode 100644 src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.SpeedOpt.cs delete mode 100644 src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.cs delete mode 100644 src/libraries/Common/src/System/Collections/Generic/SparseArrayBuilder.cs delete mode 100644 src/libraries/Common/tests/Tests/System/Collections/Generic/LargeArrayBuilderTests.cs delete mode 100644 src/libraries/System.Linq/src/System/Linq/Buffer.cs create mode 100644 src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs diff --git a/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs b/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs deleted file mode 100644 index 13970c6a0fc22b..00000000000000 --- a/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs +++ /dev/null @@ -1,88 +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.Diagnostics; -using System.Linq; - -namespace System.Collections.Generic -{ - /// - /// Internal helper functions for working with enumerables. - /// - internal static partial class EnumerableHelpers - { - /// - /// Copies items from an enumerable to an array. - /// - /// The element type of the enumerable. - /// The source enumerable. - /// The destination array. - /// The index in the array to start copying to. - /// The number of items in the enumerable. - internal static void Copy(IEnumerable source, T[] array, int arrayIndex, int count) - { - Debug.Assert(source != null); - Debug.Assert(arrayIndex >= 0); - Debug.Assert(count >= 0); - Debug.Assert(array.Length - arrayIndex >= count); - - if (source is ICollection collection) - { - Debug.Assert(collection.Count == count); - collection.CopyTo(array, arrayIndex); - return; - } - - IterativeCopy(source, array, arrayIndex, count); - } - - /// - /// Copies items from a non-collection enumerable to an array. - /// - /// The element type of the enumerable. - /// The source enumerable. - /// The destination array. - /// The index in the array to start copying to. - /// The number of items in the enumerable. - internal static void IterativeCopy(IEnumerable source, T[] array, int arrayIndex, int count) - { - Debug.Assert(source != null && !(source is ICollection)); - Debug.Assert(arrayIndex >= 0); - Debug.Assert(count >= 0); - Debug.Assert(array.Length - arrayIndex >= count); - - int endIndex = arrayIndex + count; - foreach (T item in source) - { - array[arrayIndex++] = item; - } - - Debug.Assert(arrayIndex == endIndex); - } - - /// Converts an enumerable to an array. - /// The enumerable to convert. - /// The resulting array. - internal static T[] ToArray(IEnumerable source) - { - Debug.Assert(source != null); - - if (source is ICollection collection) - { - int count = collection.Count; - if (count == 0) - { - return Array.Empty(); - } - - var result = new T[count]; - collection.CopyTo(result, arrayIndex: 0); - return result; - } - - LargeArrayBuilder builder = new(); - builder.AddRange(source); - return builder.ToArray(); - } - } -} diff --git a/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.SizeOpt.cs b/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.SizeOpt.cs deleted file mode 100644 index 50ac7317090160..00000000000000 --- a/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.SizeOpt.cs +++ /dev/null @@ -1,44 +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.Diagnostics; - -namespace System.Collections.Generic -{ - // LargeArrayBuilder.netcoreapp.cs provides a "LargeArrayBuilder" that's meant to help - // avoid allocations and copying while building up an array. But in doing so, it utilizes - // T[][] to store T[]s, which results in significant size increases for AOT builds. To - // address that, this minimal wrapper for ArrayBuilder may be used instead; it's a simple - // passthrough to ArrayBuilder, and thus doesn't incur the size increase due to the T[][]s. - - internal struct LargeArrayBuilder - { - private ArrayBuilder _builder; // mutable struct; do not make this readonly - - /// - /// Constructs a new builder. - /// - public LargeArrayBuilder() => this = default; - - public int Count => _builder.Count; - - public void Add(T item) => _builder.Add(item); - - public void AddRange(IEnumerable items) - { - Debug.Assert(items != null); - foreach (T item in items) - { - _builder.Add(item); - } - } - - public T[] ToArray() => _builder.ToArray(); - - public CopyPosition CopyTo(CopyPosition position, T[] array, int arrayIndex, int count) - { - Array.Copy(_builder.Buffer!, position.Column, array, arrayIndex, count); - return new CopyPosition(0, position.Column + count); - } - } -} diff --git a/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.SpeedOpt.cs b/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.SpeedOpt.cs deleted file mode 100644 index 99458c41dfe9aa..00000000000000 --- a/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.SpeedOpt.cs +++ /dev/null @@ -1,340 +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.Diagnostics; -using System.Runtime.CompilerServices; - -namespace System.Collections.Generic -{ - /// - /// Helper type for building dynamically-sized arrays while minimizing allocations and copying. - /// - /// The element type. - internal struct LargeArrayBuilder - { - private const int StartingCapacity = 4; - private const int ResizeLimit = 8; - - private readonly int _maxCapacity; // The maximum capacity this builder can have. - private T[] _first; // The first buffer we store items in. Resized until ResizeLimit. - private ArrayBuilder _buffers; // After ResizeLimit * 2, we store previous buffers we've filled out here. - private T[] _current; // Current buffer we're reading into. If _count <= ResizeLimit, this is _first. - private int _index; // Index into the current buffer. - private int _count; // Count of all of the items in this builder. - - /// - /// Constructs a new builder. - /// - public LargeArrayBuilder() - : this(maxCapacity: int.MaxValue) - { - } - - /// - /// Constructs a new builder with the specified maximum capacity. - /// - /// The maximum capacity this builder can have. - /// - /// Do not add more than items to this builder. - /// - public LargeArrayBuilder(int maxCapacity) - { - Debug.Assert(maxCapacity >= 0); - - this = default; - _first = _current = Array.Empty(); - _maxCapacity = maxCapacity; - } - - /// - /// Gets the number of items added to the builder. - /// - public int Count => _count; - - /// - /// Adds an item to this builder. - /// - /// The item to add. - /// - /// Use if adding to the builder is a bottleneck for your use case. - /// Otherwise, use . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(T item) - { - Debug.Assert(_maxCapacity > _count); - - int index = _index; - T[] current = _current; - - // Must be >= and not == to enable range check elimination - if ((uint)index >= (uint)current.Length) - { - AddWithBufferAllocation(item); - } - else - { - current[index] = item; - _index = index + 1; - } - - _count++; - } - - // Non-inline to improve code quality as uncommon path - [MethodImpl(MethodImplOptions.NoInlining)] - private void AddWithBufferAllocation(T item) - { - AllocateBuffer(); - _current[_index++] = item; - } - - /// - /// Adds a range of items to this builder. - /// - /// The sequence to add. - /// - /// It is the caller's responsibility to ensure that adding - /// does not cause the builder to exceed its maximum capacity. - /// - public void AddRange(IEnumerable items) - { - Debug.Assert(items != null); - - using (IEnumerator enumerator = items.GetEnumerator()) - { - T[] destination = _current; - int index = _index; - - // Continuously read in items from the enumerator, updating _count - // and _index when we run out of space. - - while (enumerator.MoveNext()) - { - T item = enumerator.Current; - - if ((uint)index >= (uint)destination.Length) - { - AddWithBufferAllocation(item, ref destination, ref index); - } - else - { - destination[index] = item; - } - - index++; - } - - // Final update to _count and _index. - _count += index - _index; - _index = index; - } - } - - // Non-inline to improve code quality as uncommon path - [MethodImpl(MethodImplOptions.NoInlining)] - private void AddWithBufferAllocation(T item, ref T[] destination, ref int index) - { - _count += index - _index; - _index = index; - AllocateBuffer(); - destination = _current; - index = _index; - _current[index] = item; - } - - /// - /// Copies the contents of this builder to the specified array. - /// - /// The destination array. - /// The index in to start copying to. - /// The number of items to copy. - public void CopyTo(T[] array, int arrayIndex, int count) - { - Debug.Assert(arrayIndex >= 0); - Debug.Assert(count >= 0 && count <= Count); - Debug.Assert(array.Length - arrayIndex >= count); - - for (int i = 0; count > 0; i++) - { - // Find the buffer we're copying from. - T[] buffer = GetBuffer(index: i); - - // Copy until we satisfy count, or we reach the end of the buffer. - int toCopy = Math.Min(count, buffer.Length); - Array.Copy(buffer, 0, array, arrayIndex, toCopy); - - // Increment variables to that position. - count -= toCopy; - arrayIndex += toCopy; - } - } - - /// - /// Copies the contents of this builder to the specified array. - /// - /// The position in this builder to start copying from. - /// The destination array. - /// The index in to start copying to. - /// The number of items to copy. - /// The position in this builder that was copied up to. - public CopyPosition CopyTo(CopyPosition position, T[] array, int arrayIndex, int count) - { - Debug.Assert(array != null); - Debug.Assert(arrayIndex >= 0); - Debug.Assert(count > 0 && count <= Count); - Debug.Assert(array.Length - arrayIndex >= count); - - // Go through each buffer, which contains one 'row' of items. - // The index in each buffer is referred to as the 'column'. - - /* - * Visual representation: - * - * C0 C1 C2 .. C31 .. C63 - * R0: [0] [1] [2] .. [31] - * R1: [32] [33] [34] .. [63] - * R2: [64] [65] [66] .. [95] .. [127] - */ - - int row = position.Row; - int column = position.Column; - - T[] buffer = GetBuffer(row); - int copied = CopyToCore(buffer, column); - - if (count == 0) - { - return new CopyPosition(row, column + copied).Normalize(buffer.Length); - } - - do - { - buffer = GetBuffer(++row); - copied = CopyToCore(buffer, 0); - } while (count > 0); - - return new CopyPosition(row, copied).Normalize(buffer.Length); - - int CopyToCore(T[] sourceBuffer, int sourceIndex) - { - Debug.Assert(sourceBuffer.Length > sourceIndex); - - // Copy until we satisfy `count` or reach the end of the current buffer. - int copyCount = Math.Min(sourceBuffer.Length - sourceIndex, count); - Array.Copy(sourceBuffer, sourceIndex, array, arrayIndex, copyCount); - - arrayIndex += copyCount; - count -= copyCount; - - return copyCount; - } - } - - /// - /// Retrieves the buffer at the specified index. - /// - /// The index of the buffer. - public T[] GetBuffer(int index) - { - Debug.Assert(index >= 0 && index < _buffers.Count + 2); - - return index == 0 ? _first : - index <= _buffers.Count ? _buffers[index - 1] : - _current; - } - - /// - /// Adds an item to this builder. - /// - /// The item to add. - /// - /// Use if adding to the builder is a bottleneck for your use case. - /// Otherwise, use . - /// - [MethodImpl(MethodImplOptions.NoInlining)] - public void SlowAdd(T item) => Add(item); - - /// - /// Creates an array from the contents of this builder. - /// - public T[] ToArray() - { - if (TryMove(out T[] array)) - { - // No resizing to do. - return array; - } - - array = new T[_count]; - CopyTo(array, 0, _count); - return array; - } - - /// - /// Attempts to transfer this builder into an array without copying. - /// - /// The transferred array, if the operation succeeded. - /// true if the operation succeeded; otherwise, false. - public bool TryMove(out T[] array) - { - array = _first; - return _count == _first.Length; - } - - private void AllocateBuffer() - { - // - On the first few adds, simply resize _first. - // - When we pass ResizeLimit, allocate ResizeLimit elements for _current - // and start reading into _current. Set _index to 0. - // - When _current runs out of space, add it to _buffers and repeat the - // above step, except with _current.Length * 2. - // - Make sure we never pass _maxCapacity in all of the above steps. - - Debug.Assert((uint)_maxCapacity > (uint)_count); - Debug.Assert(_index == _current.Length, $"{nameof(AllocateBuffer)} was called, but there's more space."); - - // If _count is int.MinValue, we want to go down the other path which will raise an exception. - if ((uint)_count < (uint)ResizeLimit) - { - // We haven't passed ResizeLimit. Resize _first, copying over the previous items. - Debug.Assert(_current == _first && _count == _first.Length); - - int nextCapacity = Math.Min(_count == 0 ? StartingCapacity : _count * 2, _maxCapacity); - - _current = new T[nextCapacity]; - Array.Copy(_first, _current, _count); - _first = _current; - } - else - { - Debug.Assert(_maxCapacity > ResizeLimit); - Debug.Assert(_count == ResizeLimit ^ _current != _first); - - int nextCapacity; - if (_count == ResizeLimit) - { - nextCapacity = ResizeLimit; - } - else - { - // Example scenario: Let's say _count == 64. - // Then our buffers look like this: | 8 | 8 | 16 | 32 | - // As you can see, our count will be just double the last buffer. - // Now, say _maxCapacity is 100. We will find the right amount to allocate by - // doing min(64, 100 - 64). The lhs represents double the last buffer, - // the rhs the limit minus the amount we've already allocated. - - Debug.Assert(_count >= ResizeLimit * 2); - Debug.Assert(_count == _current.Length * 2); - - _buffers.Add(_current); - nextCapacity = Math.Min(_count, _maxCapacity - _count); - } - - _current = new T[nextCapacity]; - _index = 0; - } - } - } -} diff --git a/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.cs deleted file mode 100644 index bf28fb607b239b..00000000000000 --- a/src/libraries/Common/src/System/Collections/Generic/LargeArrayBuilder.cs +++ /dev/null @@ -1,62 +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.Diagnostics; - -namespace System.Collections.Generic -{ - /// - /// Represents a position within a . - /// - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal readonly struct CopyPosition - { - /// - /// Constructs a new . - /// - /// The index of the buffer to select. - /// The index within the buffer to select. - internal CopyPosition(int row, int column) - { - Debug.Assert(row >= 0); - Debug.Assert(column >= 0); - - Row = row; - Column = column; - } - - /// - /// Represents a position at the start of a . - /// - public static CopyPosition Start => default(CopyPosition); - - /// - /// The index of the buffer to select. - /// - internal int Row { get; } - - /// - /// The index within the buffer to select. - /// - internal int Column { get; } - - /// - /// If this position is at the end of the current buffer, returns the position - /// at the start of the next buffer. Otherwise, returns this position. - /// - /// The length of the current buffer. - public CopyPosition Normalize(int endColumn) - { - Debug.Assert(Column <= endColumn); - - return Column == endColumn ? - new CopyPosition(Row + 1, 0) : - this; - } - - /// - /// Gets a string suitable for display in the debugger. - /// - private string DebuggerDisplay => $"[{Row}, {Column}]"; - } -} diff --git a/src/libraries/Common/src/System/Collections/Generic/SparseArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/SparseArrayBuilder.cs deleted file mode 100644 index b027b21cdaccf8..00000000000000 --- a/src/libraries/Common/src/System/Collections/Generic/SparseArrayBuilder.cs +++ /dev/null @@ -1,224 +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.Diagnostics; - -namespace System.Collections.Generic -{ - /// - /// Represents a reserved region within a . - /// - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal readonly struct Marker - { - /// - /// Constructs a new marker. - /// - /// The number of items to reserve. - /// The index in the builder where this marker starts. - public Marker(int count, int index) - { - Debug.Assert(count >= 0); - Debug.Assert(index >= 0); - - Count = count; - Index = index; - } - - /// - /// The number of items to reserve. - /// - public int Count { get; } - - /// - /// The index in the builder where this marker starts. - /// - public int Index { get; } - - /// - /// Gets a string suitable for display in the debugger. - /// - private string DebuggerDisplay => $"{nameof(Index)} = {Index}, {nameof(Count)} = {Count}"; - } - - /// - /// Helper type for building arrays where sizes of certain segments are known in advance. - /// - /// The element type. - internal struct SparseArrayBuilder - { - /// - /// The underlying builder that stores items from non-reserved regions. - /// - /// - /// This field is a mutable struct; do not mark it readonly. - /// - private LargeArrayBuilder _builder; - - /// - /// The list of reserved regions within this builder. - /// - /// - /// This field is a mutable struct; do not mark it readonly. - /// - private ArrayBuilder _markers; - - /// - /// The total number of reserved slots within this builder. - /// - private int _reservedCount; - - /// - /// Constructs a new builder. - /// - public SparseArrayBuilder() - { - this = default; - _builder = new LargeArrayBuilder(); - } - - /// - /// The total number of items in this builder, including reserved regions. - /// - public int Count => checked(_builder.Count + _reservedCount); - - /// - /// The list of reserved regions in this builder. - /// - public ArrayBuilder Markers => _markers; - - /// - /// Adds an item to this builder. - /// - /// The item to add. - public void Add(T item) => _builder.Add(item); - - /// - /// Adds a range of items to this builder. - /// - /// The sequence to add. - public void AddRange(IEnumerable items) => _builder.AddRange(items); - - /// - /// Copies the contents of this builder to the specified array. - /// - /// The destination array. - /// The index in to start copying to. - /// The number of items to copy. - public void CopyTo(T[] array, int arrayIndex, int count) - { - Debug.Assert(array != null); - Debug.Assert(arrayIndex >= 0); - Debug.Assert(count >= 0 && count <= Count); - Debug.Assert(array.Length - arrayIndex >= count); - - int copied = 0; - var position = CopyPosition.Start; - - for (int i = 0; i < _markers.Count; i++) - { - Marker marker = _markers[i]; - - // During this iteration, copy until we satisfy `count` or reach the marker. - int toCopy = Math.Min(marker.Index - copied, count); - - if (toCopy > 0) - { - position = _builder.CopyTo(position, array, arrayIndex, toCopy); - - arrayIndex += toCopy; - copied += toCopy; - count -= toCopy; - } - - if (count == 0) - { - return; - } - - // We hit our marker. Advance until we satisfy `count` or fulfill `marker.Count`. - int reservedCount = Math.Min(marker.Count, count); - - arrayIndex += reservedCount; - copied += reservedCount; - count -= reservedCount; - } - - if (count > 0) - { - // Finish copying after the final marker. - _builder.CopyTo(position, array, arrayIndex, count); - } - } - - /// - /// Reserves a region starting from the current index. - /// - /// The number of items to reserve. - /// - /// This method will not make optimizations if - /// is zero; the caller is responsible for doing so. The reason for this - /// is that the number of markers needs to match up exactly with the number - /// of times was called. - /// - public void Reserve(int count) - { - Debug.Assert(count >= 0); - - _markers.Add(new Marker(count: count, index: Count)); - - checked - { - _reservedCount += count; - } - } - - /// - /// Reserves a region if the items' count can be predetermined; otherwise, adds the items to this builder. - /// - /// The items to reserve or add. - /// true if the items were reserved; otherwise, false. - /// - /// If the items' count is predetermined to be 0, no reservation is made and the return value is false. - /// The effect is the same as if the items were added, since adding an empty collection does nothing. - /// - public bool ReserveOrAdd(IEnumerable items) - { - int itemCount; - if (System.Linq.Enumerable.TryGetNonEnumeratedCount(items, out itemCount)) - { - if (itemCount > 0) - { - Reserve(itemCount); - return true; - } - } - else - { - AddRange(items); - } - return false; - } - - /// - /// Creates an array from the contents of this builder. - /// - /// - /// Regions created with will be default-initialized. - /// - public T[] ToArray() - { - // If no regions were reserved, there are no 'gaps' we need to add to the array. - // In that case, we can just call ToArray on the underlying builder. - if (_markers.Count == 0) - { - Debug.Assert(_reservedCount == 0); - return _builder.ToArray(); - } - - var array = new T[Count]; - CopyTo(array, 0, array.Length); - return array; - } - } -} diff --git a/src/libraries/Common/tests/Common.Tests.csproj b/src/libraries/Common/tests/Common.Tests.csproj index eae8a369ceff58..cded1098de094c 100644 --- a/src/libraries/Common/tests/Common.Tests.csproj +++ b/src/libraries/Common/tests/Common.Tests.csproj @@ -23,10 +23,6 @@ Link="Common\System\StringExtensions.cs" /> - - - diff --git a/src/libraries/Common/tests/Tests/System/Collections/Generic/LargeArrayBuilderTests.cs b/src/libraries/Common/tests/Tests/System/Collections/Generic/LargeArrayBuilderTests.cs deleted file mode 100644 index e3008478057cc3..00000000000000 --- a/src/libraries/Common/tests/Tests/System/Collections/Generic/LargeArrayBuilderTests.cs +++ /dev/null @@ -1,182 +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.Diagnostics; -using System.Linq; -using Xunit; - -namespace System.Collections.Generic.Tests -{ - public abstract class LargeArrayBuilderTests where TGenerator : IGenerator, new() - { - private static readonly TGenerator s_generator = new TGenerator(); - - [Fact] - public void Constructor() - { - var builder = new LargeArrayBuilder(); - - Assert.Equal(0, builder.Count); - Assert.Same(Array.Empty(), builder.ToArray()); - } - - [Theory] - [MemberData(nameof(EnumerableData))] - public void AddCountAndToArray(IEnumerable seed) - { - var builder1 = new LargeArrayBuilder(); - var builder2 = new LargeArrayBuilder(); - - int count = 0; - foreach (T item in seed) - { - count++; - - builder1.Add(item); - builder2.SlowAdd(item); // Verify SlowAdd has exactly the same effect as Add. - - Assert.Equal(count, builder1.Count); - Assert.Equal(count, builder2.Count); - - Assert.Equal(seed.Take(count), builder1.ToArray()); - Assert.Equal(seed.Take(count), builder2.ToArray()); - } - } - - [Theory] - [MemberData(nameof(MaxCapacityData))] - public void MaxCapacity(IEnumerable seed, int maxCapacity) - { - var builder = new LargeArrayBuilder(maxCapacity); - - for (int i = 0; i < maxCapacity; i++) - { - builder.Add(seed.ElementAt(i)); - - int count = i + 1; - Assert.Equal(count, builder.Count); - Assert.Equal(seed.Take(count), builder.ToArray()); - } - } - - [Theory] - [MemberData(nameof(EnumerableData))] - public void AddRange(IEnumerable seed) - { - var builder = new LargeArrayBuilder(); - - // Call AddRange multiple times and verify contents w/ each iteration. - for (int i = 1; i <= 10; i++) - { - builder.AddRange(seed); - - IEnumerable expected = Enumerable.Repeat(seed, i).SelectMany(s => s); - Assert.Equal(expected, builder.ToArray()); - } - } - - [Theory] - [MemberData(nameof(CopyToData))] - public void CopyTo(IEnumerable seed, int index, int count) - { - var array = new T[seed.Count()]; - - var builder = new LargeArrayBuilder(); - builder.AddRange(seed); - builder.CopyTo(array, index, count); - - // Ensure we don't copy out of bounds by verifying contents outside the copy area, too. - IEnumerable prefix = array.Take(index); - IEnumerable suffix = array.Skip(index + count); - IEnumerable actual = array.Skip(index).Take(count); - - Assert.Equal(Enumerable.Repeat(default(T), index), prefix); - Assert.Equal(Enumerable.Repeat(default(T), array.Length - index - count), suffix); - Assert.Equal(seed.Take(count), actual); - } - - public static TheoryData> EnumerableData() - { - var data = new TheoryData>(); - - foreach (int count in Counts) - { - data.Add(Enumerable.Repeat(default(T), count)); - - // Test perf: Capture the items into a List here so we - // only enumerate the sequence once. - data.Add(s_generator.GenerateEnumerable(count).ToList()); - } - - return data; - } - - public static TheoryData, int> MaxCapacityData() - { - var data = new TheoryData, int>(); - - IEnumerable> enumerables = EnumerableData().Select(array => array[0]).Cast>(); - - foreach (IEnumerable enumerable in enumerables) - { - int count = enumerable.Count(); - data.Add(enumerable, count); - } - - return data; - } - - public static TheoryData, int, int> CopyToData() - { - var data = new TheoryData, int, int>(); - - IEnumerable> enumerables = EnumerableData().Select(array => array[0]).Cast>(); - - foreach (IEnumerable enumerable in enumerables) - { - int count = enumerable.Count(); - data.Add(enumerable, 0, count); - - // We want to make sure CopyTo works with different indices/counts too. - data.Add(enumerable, 0, count / 2); - data.Add(enumerable, count / 2, count / 2); - data.Add(enumerable, count / 4, count / 2); - } - - return data; - } - - private static IEnumerable Counts - { - get - { - for (int i = 0; i < 6; i++) - { - int powerOfTwo = 1 << i; - - // Return numbers of the form 2^N - 1, 2^N and 2^N + 1 - // This should cover most of the interesting cases - yield return powerOfTwo - 1; - yield return powerOfTwo; - yield return powerOfTwo + 1; - } - } - } - } - - public class LargeArrayBuilderTestsInt32 : LargeArrayBuilderTests - { - public sealed class Generator : IGenerator - { - public int Generate(int seed) => seed; - } - } - - public class LargeArrayBuilderTestsString : LargeArrayBuilderTests - { - public sealed class Generator : IGenerator - { - public string Generate(int seed) => seed.ToString(); - } - } -} diff --git a/src/libraries/System.Linq/System.Linq.sln b/src/libraries/System.Linq/System.Linq.sln index b6b2290abd290c..5f3c4a80c22450 100644 --- a/src/libraries/System.Linq/System.Linq.sln +++ b/src/libraries/System.Linq/System.Linq.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34431.11 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{AF1B1B01-A4EC-45F4-AE51-CC1FA7892181}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Collections", "..\System.Collections\ref\System.Collections.csproj", "{3A8560D8-0E79-4BDE-802A-C96C7FE98258}" @@ -35,11 +39,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E6102BFA-080 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{E61195C4-72B4-47A3-AC98-1F896A0C770F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{84E98F7C-FA2B-4048-AB7C-9FCDEA9CD37E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{84E98F7C-FA2B-4048-AB7C-9FCDEA9CD37E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{8CA90AB2-58B9-45E7-A684-EDB60C6924B0}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8CA90AB2-58B9-45E7-A684-EDB60C6924B0}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{7C5B49B9-F7D9-41FB-A8FA-94328BDDCCD1}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{7C5B49B9-F7D9-41FB-A8FA-94328BDDCCD1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{0ADC596A-5B2E-4E5F-B5B5-DEB65A6C7E9D}" EndProject @@ -111,24 +115,28 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {AF1B1B01-A4EC-45F4-AE51-CC1FA7892181} = {E291F4BF-7B8B-45AD-88F5-FB8B8380C126} - {80A4051B-4A36-4A8B-BA43-A5AB8AA959F3} = {E291F4BF-7B8B-45AD-88F5-FB8B8380C126} {3A8560D8-0E79-4BDE-802A-C96C7FE98258} = {0BCB262A-FC13-4A48-BB0B-9FA293594701} {7E4C1F09-B4F2-470E-9E7B-2C386E93D657} = {0BCB262A-FC13-4A48-BB0B-9FA293594701} - {D3160C37-FC48-4907-8F4A-F584ED12B275} = {0BCB262A-FC13-4A48-BB0B-9FA293594701} {14B966BB-CE23-4432-ADBB-89974389AC1D} = {E6102BFA-0803-4AB7-8E91-C4D3B42AFA20} + {80A4051B-4A36-4A8B-BA43-A5AB8AA959F3} = {E291F4BF-7B8B-45AD-88F5-FB8B8380C126} {9A13A12F-C924-43AF-94AF-6F1B33582D27} = {E61195C4-72B4-47A3-AC98-1F896A0C770F} {4BEC631E-B5FD-453F-82A0-C95C461798EA} = {E61195C4-72B4-47A3-AC98-1F896A0C770F} {C8F0459C-15D5-4624-8CE4-E93ADF96A28C} = {E61195C4-72B4-47A3-AC98-1F896A0C770F} + {D3160C37-FC48-4907-8F4A-F584ED12B275} = {0BCB262A-FC13-4A48-BB0B-9FA293594701} {E0CA3ED5-EE6C-4F7C-BCE7-EFB1D64A9CD1} = {84E98F7C-FA2B-4048-AB7C-9FCDEA9CD37E} {3EFB74E7-616A-48C1-B43B-3F89AA5013E6} = {84E98F7C-FA2B-4048-AB7C-9FCDEA9CD37E} - {84E98F7C-FA2B-4048-AB7C-9FCDEA9CD37E} = {0ADC596A-5B2E-4E5F-B5B5-DEB65A6C7E9D} {28ABC524-ACEE-4183-A64A-49E3DC830595} = {8CA90AB2-58B9-45E7-A684-EDB60C6924B0} {721DB3D9-8221-424E-BE29-084CDD20D26E} = {8CA90AB2-58B9-45E7-A684-EDB60C6924B0} - {8CA90AB2-58B9-45E7-A684-EDB60C6924B0} = {0ADC596A-5B2E-4E5F-B5B5-DEB65A6C7E9D} {E19B8772-2DBD-4274-8190-F3CC0242A1C0} = {7C5B49B9-F7D9-41FB-A8FA-94328BDDCCD1} + {84E98F7C-FA2B-4048-AB7C-9FCDEA9CD37E} = {0ADC596A-5B2E-4E5F-B5B5-DEB65A6C7E9D} + {8CA90AB2-58B9-45E7-A684-EDB60C6924B0} = {0ADC596A-5B2E-4E5F-B5B5-DEB65A6C7E9D} {7C5B49B9-F7D9-41FB-A8FA-94328BDDCCD1} = {0ADC596A-5B2E-4E5F-B5B5-DEB65A6C7E9D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4970D79-BF1C-4343-9070-B409DBB69F93} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{3efb74e7-616a-48c1-b43b-3f89aa5013e6}*SharedItemsImports = 5 + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{721db3d9-8221-424e-be29-084cdd20d26e}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/libraries/System.Linq/src/System.Linq.csproj b/src/libraries/System.Linq/src/System.Linq.csproj index 8ccb640b191078..a361e0fe53c33f 100644 --- a/src/libraries/System.Linq/src/System.Linq.csproj +++ b/src/libraries/System.Linq/src/System.Linq.csproj @@ -15,8 +15,6 @@ - @@ -38,26 +36,13 @@ - - - - - - - @@ -90,6 +75,7 @@ + diff --git a/src/libraries/System.Linq/src/System/Linq/AppendPrepend.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/AppendPrepend.SpeedOpt.cs index caf83bee1fcaac..80ee23998603fa 100644 --- a/src/libraries/System.Linq/src/System/Linq/AppendPrepend.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/AppendPrepend.SpeedOpt.cs @@ -22,22 +22,45 @@ private sealed partial class AppendPrepend1Iterator private TSource[] LazyToArray() { Debug.Assert(GetCount(onlyIfCheap: true) == -1); + TSource[] result; - LargeArrayBuilder builder = new(); - - if (!_appending) + if (_source is ICollection c) { - builder.SlowAdd(_item); + // Allocate an array of the exact size needed. We have a collection + // with an additional item either before it or after it; copy them + // all to the new array appropriately. + result = new TSource[c.Count + 1]; + if (_appending) + { + c.CopyTo(result, 0); + result[^1] = _item; + } + else + { + c.CopyTo(result, 1); + result[0] = _item; + } } - - builder.AddRange(_source); - - if (_appending) + else { - builder.SlowAdd(_item); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + if (_appending) + { + builder.AddNonICollectionRange(_source); + builder.Add(_item); + } + else + { + builder.Add(_item); + builder.AddNonICollectionRange(_source); + } + + result = builder.ToArray(); + builder.Dispose(); } - return builder.ToArray(); + return result; } public override TSource[] ToArray() @@ -60,11 +83,21 @@ public override TSource[] ToArray() index = 1; } - EnumerableHelpers.Copy(_source, array, index, count - 1); + if (_source is ICollection collection) + { + collection.CopyTo(array, index); + } + else + { + foreach (TSource item in _source) + { + array[index++] = item; + } + } if (_appending) { - array[array.Length - 1] = _item; + array[^1] = _item; } return array; @@ -113,26 +146,35 @@ private TSource[] LazyToArray() { Debug.Assert(GetCount(onlyIfCheap: true) == -1); - SparseArrayBuilder builder = new(); - - if (_prepended != null) + if (_source is ICollection c) { - builder.Reserve(_prependCount); - } + var result = new TSource[checked(_prependCount + c.Count + _appendCount)]; - builder.AddRange(_source); + _prepended?.Fill(result); + c.CopyTo(result, _prependCount); + _appended?.FillReversed(result); - if (_appended != null) - { - builder.Reserve(_appendCount); + return result; } + else + { + // Create the new builder with the prepended content and source content. Then + // build the resulting array with enough space to also hold any appended content, + // and write the appended content directly into the resulting array. + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + for (SingleLinkedNode? node = _prepended; node is not null; node = node.Linked) + { + builder.Add(node.Item); + } + builder.AddNonICollectionRange(_source); - TSource[] array = builder.ToArray(); - - _prepended?.Fill(array); - _appended?.FillReversed(array); + TSource[] result = builder.ToArray(_appendCount); + builder.Dispose(); - return array; + _appended?.FillReversed(result); + return result; + } } public override TSource[] ToArray() diff --git a/src/libraries/System.Linq/src/System/Linq/Average.cs b/src/libraries/System.Linq/src/System/Linq/Average.cs index e71b3558d71493..df0f34cf098eb6 100644 --- a/src/libraries/System.Linq/src/System/Linq/Average.cs +++ b/src/libraries/System.Linq/src/System/Linq/Average.cs @@ -10,6 +10,11 @@ public static partial class Enumerable { public static double Average(this IEnumerable source) { + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + if (source.TryGetSpan(out ReadOnlySpan span)) { // Int32 is special-cased separately from the rest of the types as it can be vectorized: @@ -79,6 +84,11 @@ private static TResult Average(this IEnumerable< where TAccumulator : struct, INumber where TResult : struct, INumber { + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + if (source.TryGetSpan(out ReadOnlySpan span)) { if (span.IsEmpty) diff --git a/src/libraries/System.Linq/src/System/Linq/Buffer.cs b/src/libraries/System.Linq/src/System/Linq/Buffer.cs deleted file mode 100644 index 1821a87b100d52..00000000000000 --- a/src/libraries/System.Linq/src/System/Linq/Buffer.cs +++ /dev/null @@ -1,42 +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 -{ - /// - /// A buffer into which the contents of an can be stored. - /// - /// The type of the buffer's elements. - internal readonly struct Buffer - { - /// - /// The stored items. - /// - internal readonly TElement[] _items; - - /// - /// The number of stored items. - /// - internal readonly int _count; - - /// - /// Fully enumerates the provided enumerable and stores its items into an array. - /// - /// The enumerable to be store. - internal Buffer(IEnumerable source) - { - if (source is IIListProvider iterator) - { - TElement[] array = iterator.ToArray(); - _items = array; - _count = array.Length; - } - else - { - _items = EnumerableHelpers.ToArray(source, out _count); - } - } - } -} diff --git a/src/libraries/System.Linq/src/System/Linq/Concat.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/Concat.SpeedOpt.cs index 70dfe8d5c230da..b0adeaf5cb9c2a 100644 --- a/src/libraries/System.Linq/src/System/Linq/Concat.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Concat.SpeedOpt.cs @@ -38,27 +38,56 @@ public override int GetCount(bool onlyIfCheap) public override TSource[] ToArray() { - SparseArrayBuilder builder = new(); + ICollection? firstCollection = _first as ICollection; + ICollection? secondCollection = _second as ICollection; - bool reservedFirst = builder.ReserveOrAdd(_first); - bool reservedSecond = builder.ReserveOrAdd(_second); + if (firstCollection is not null && secondCollection is not null) + { + // Both sources are ICollection, so we know their sizes and can just copy them. + int firstCount = firstCollection.Count; + TSource[] result = new TSource[checked(firstCount + secondCollection.Count)]; - TSource[] array = builder.ToArray(); + firstCollection.CopyTo(result, 0); + secondCollection.CopyTo(result, firstCount); - if (reservedFirst) - { - Marker marker = builder.Markers.First(); - Debug.Assert(marker.Index == 0); - EnumerableHelpers.Copy(_first, array, 0, marker.Count); + return result; } - - if (reservedSecond) + else { - Marker marker = builder.Markers.Last(); - EnumerableHelpers.Copy(_second, array, marker.Index, marker.Count); - } + // We don't know the sizes of at least one if not both sources, so we need a builder. + // If we don't know the sizes of both, we'll just append each into the builder and + // use the builder to create the overall array. If we know the size of one, we'll + // only buffer the other. + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + TSource[] result; + + if (firstCollection is not null) + { + int firstCount = firstCollection.Count; + builder.AddNonICollectionRange(_second); + result = new TSource[checked(firstCount + builder.Count)]; + firstCollection.CopyTo(result, 0); + builder.ToSpan(result.AsSpan(firstCount)); + } + else if (secondCollection is not null) + { + int secondCount = secondCollection.Count; + builder.AddNonICollectionRange(_first); + result = new TSource[checked(builder.Count + secondCount)]; + builder.ToSpan(result); + secondCollection.CopyTo(result, result.Length - secondCount); + } + else + { + builder.AddNonICollectionRange(_first); + builder.AddNonICollectionRange(_second); + result = builder.ToArray(); + } - return array; + builder.Dispose(); + return result; + } } } @@ -100,10 +129,12 @@ public override int GetCount(bool onlyIfCheap) private TSource[] LazyToArray() { + // All of the sources being ICollection is handled by PreallocatingToArray, so if we're here, + // at least one source isn't an ICollection. Debug.Assert(!_hasOnlyCollections); - SparseArrayBuilder builder = new(); - ArrayBuilder deferredCopies = default; + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); for (int i = 0; ; i++) { @@ -111,30 +142,19 @@ private TSource[] LazyToArray() // quadratic behavior, because we need to add the sources in order. // On the bright side, the bottleneck will usually be iterating, buffering, and copying // each of the enumerables, so this shouldn't be a noticeable perf hit for most scenarios. - IEnumerable? source = GetEnumerable(i); if (source == null) { break; } - if (builder.ReserveOrAdd(source)) - { - deferredCopies.Add(i); - } + builder.AddRange(source); } - TSource[] array = builder.ToArray(); + TSource[] result = builder.ToArray(); + builder.Dispose(); - ArrayBuilder markers = builder.Markers; - for (int i = 0; i < markers.Count; i++) - { - Marker marker = markers[i]; - IEnumerable source = GetEnumerable(deferredCopies[i])!; - EnumerableHelpers.Copy(source, array, marker.Index, marker.Count); - } - - return array; + return result; } private TSource[] PreallocatingToArray() diff --git a/src/libraries/System.Linq/src/System/Linq/Enumerable.cs b/src/libraries/System.Linq/src/System/Linq/Enumerable.cs index 029583b8f4b346..b459216096b35a 100644 --- a/src/libraries/System.Linq/src/System/Linq/Enumerable.cs +++ b/src/libraries/System.Linq/src/System/Linq/Enumerable.cs @@ -24,18 +24,8 @@ internal static Span SetCountAndGetSpan(List list, int count) /// Validates that source is not null and then tries to extract a span from the source. [MethodImpl(MethodImplOptions.AggressiveInlining)] // fast type checks that don't add a lot of overhead - private static bool TryGetSpan(this IEnumerable source, out ReadOnlySpan span) - // This constraint isn't required, but the overheads involved here can be more substantial when TSource - // is a reference type and generic implementations are shared. So for now we're protecting ourselves - // and forcing a conscious choice to remove this in the future, at which point it should be paired with - // sufficient performance testing. - where TSource : struct + internal static bool TryGetSpan(this IEnumerable source, out ReadOnlySpan span) { - if (source is null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); - } - // Use `GetType() == typeof(...)` rather than `is` to avoid cast helpers. This is measurably cheaper // but does mean we could end up missing some rare cases where we could get a span but don't (e.g. a uint[] // masquerading as an int[]). That's an acceptable tradeoff. The Unsafe usage is only after we've diff --git a/src/libraries/System.Linq/src/System/Linq/Max.cs b/src/libraries/System.Linq/src/System/Linq/Max.cs index bd08684270f6c7..20ac68d69570fe 100644 --- a/src/libraries/System.Linq/src/System/Linq/Max.cs +++ b/src/libraries/System.Linq/src/System/Linq/Max.cs @@ -99,6 +99,11 @@ private static T MaxFloat(this IEnumerable source) where T : struct, IFloa { T value; + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + if (source.TryGetSpan(out ReadOnlySpan span)) { if (span.IsEmpty) @@ -218,6 +223,11 @@ public static decimal Max(this IEnumerable source) { decimal value; + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + if (source.TryGetSpan(out ReadOnlySpan span)) { if (span.IsEmpty) diff --git a/src/libraries/System.Linq/src/System/Linq/MaxMin.cs b/src/libraries/System.Linq/src/System/Linq/MaxMin.cs index 47974c48a1c6fc..15da2e3bfa7295 100644 --- a/src/libraries/System.Linq/src/System/Linq/MaxMin.cs +++ b/src/libraries/System.Linq/src/System/Linq/MaxMin.cs @@ -25,6 +25,11 @@ private static T MinMaxInteger(this IEnumerable source) { T value; + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + if (source.TryGetSpan(out ReadOnlySpan span)) { if (span.IsEmpty) diff --git a/src/libraries/System.Linq/src/System/Linq/Min.cs b/src/libraries/System.Linq/src/System/Linq/Min.cs index 6c81274f3013c8..765d9f56c3dc4f 100644 --- a/src/libraries/System.Linq/src/System/Linq/Min.cs +++ b/src/libraries/System.Linq/src/System/Linq/Min.cs @@ -81,6 +81,11 @@ private static T MinFloat(this IEnumerable source) where T : struct, IFloa { T value; + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + if (source.TryGetSpan(out ReadOnlySpan span)) { if (span.IsEmpty) @@ -197,6 +202,11 @@ public static decimal Min(this IEnumerable source) { decimal value; + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + if (source.TryGetSpan(out ReadOnlySpan span)) { if (span.IsEmpty) diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs index e9cb982c2bc43c..79b78ec24fd1fb 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs @@ -11,38 +11,36 @@ internal abstract partial class OrderedEnumerable : IPartition buffer = new Buffer(_source); - - int count = buffer._count; - if (count == 0) + TElement[] buffer = _source.ToArray(); + if (buffer.Length == 0) { - return buffer._items; + return buffer; } - TElement[] array = new TElement[count]; + TElement[] array = new TElement[buffer.Length]; Fill(buffer, array); return array; } public virtual List ToList() { - Buffer buffer = new Buffer(_source); - int count = buffer._count; - List list = new List(count); - if (count > 0) + TElement[] buffer = _source.ToArray(); + + List list = new(); + if (buffer.Length > 0) { - Fill(buffer, Enumerable.SetCountAndGetSpan(list, count)); + Fill(buffer, Enumerable.SetCountAndGetSpan(list, buffer.Length)); } return list; } - private void Fill(Buffer buffer, Span destination) + private void Fill(TElement[] buffer, Span destination) { int[] map = SortedMap(buffer); for (int i = 0; i < destination.Length; i++) { - destination[i] = buffer._items[map[i]]; + destination[i] = buffer[map[i]]; } } @@ -58,21 +56,20 @@ public int GetCount(bool onlyIfCheap) internal TElement[] ToArray(int minIdx, int maxIdx) { - Buffer buffer = new Buffer(_source); - int count = buffer._count; - if (count <= minIdx) + TElement[] buffer = _source.ToArray(); + if (buffer.Length <= minIdx) { return Array.Empty(); } - if (count <= maxIdx) + if (buffer.Length <= maxIdx) { - maxIdx = count - 1; + maxIdx = buffer.Length - 1; } if (minIdx == maxIdx) { - return new TElement[] { GetEnumerableSorter().ElementAt(buffer._items, count, minIdx) }; + return [GetEnumerableSorter().ElementAt(buffer, buffer.Length, minIdx)]; } TElement[] array = new TElement[maxIdx - minIdx + 1]; @@ -84,35 +81,34 @@ internal TElement[] ToArray(int minIdx, int maxIdx) internal List ToList(int minIdx, int maxIdx) { - Buffer buffer = new Buffer(_source); - int count = buffer._count; - if (count <= minIdx) + TElement[] buffer = _source.ToArray(); + if (buffer.Length <= minIdx) { return new List(); } - if (count <= maxIdx) + if (buffer.Length <= maxIdx) { - maxIdx = count - 1; + maxIdx = buffer.Length - 1; } if (minIdx == maxIdx) { - return new List(1) { GetEnumerableSorter().ElementAt(buffer._items, count, minIdx) }; + return new List(1) { GetEnumerableSorter().ElementAt(buffer, buffer.Length, minIdx) }; } - List list = new List(maxIdx - minIdx + 1); + List list = new(); Fill(minIdx, maxIdx, buffer, Enumerable.SetCountAndGetSpan(list, maxIdx - minIdx + 1)); return list; } - private void Fill(int minIdx, int maxIdx, Buffer buffer, Span destination) + private void Fill(int minIdx, int maxIdx, TElement[] buffer, Span destination) { int[] map = SortedMap(buffer, minIdx, maxIdx); int idx = 0; while (minIdx <= maxIdx) { - destination[idx] = buffer._items[map[minIdx]]; + destination[idx] = buffer[map[minIdx]]; ++idx; ++minIdx; } @@ -147,12 +143,11 @@ internal int GetCount(int minIdx, int maxIdx, bool onlyIfCheap) if (index > 0) { - Buffer buffer = new Buffer(_source); - int count = buffer._count; - if (index < count) + TElement[] buffer = _source.ToArray(); + if (index < buffer.Length) { found = true; - return GetEnumerableSorter().ElementAt(buffer._items, count, index); + return GetEnumerableSorter().ElementAt(buffer, buffer.Length, index); } } @@ -216,29 +211,30 @@ internal int GetCount(int minIdx, int maxIdx, bool onlyIfCheap) public TElement? TryGetLast(int minIdx, int maxIdx, out bool found) { - Buffer buffer = new Buffer(_source); - int count = buffer._count; - if (minIdx >= count) + TElement[] buffer = _source.ToArray(); + if (minIdx < buffer.Length) { - found = false; - return default; + found = true; + return (maxIdx < buffer.Length - 1) ? + GetEnumerableSorter().ElementAt(buffer, buffer.Length, maxIdx) : + Last(buffer); } - found = true; - return (maxIdx < count - 1) ? GetEnumerableSorter().ElementAt(buffer._items, count, maxIdx) : Last(buffer); + found = false; + return default; } - private TElement Last(Buffer buffer) + private TElement Last(TElement[] items) { CachingComparer comparer = GetComparer(); - TElement[] items = buffer._items; - int count = buffer._count; + TElement value = items[0]; comparer.SetElement(value); - for (int i = 1; i != count; ++i) + + for (int i = 1; i < items.Length; ++i) { TElement x = items[i]; - if (comparer.Compare(x, false) >= 0) + if (comparer.Compare(x, cacheLower: false) >= 0) { value = x; } diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs index 1bcdb6b2b780d1..2828d765fdfaea 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs @@ -13,28 +13,28 @@ internal abstract partial class OrderedEnumerable : IOrderedEnumerable protected OrderedEnumerable(IEnumerable source) => _source = source; - private int[] SortedMap(Buffer buffer) => GetEnumerableSorter().Sort(buffer._items, buffer._count); + private int[] SortedMap(TElement[] buffer) => GetEnumerableSorter().Sort(buffer, buffer.Length); - private int[] SortedMap(Buffer buffer, int minIdx, int maxIdx) => - GetEnumerableSorter().Sort(buffer._items, buffer._count, minIdx, maxIdx); + private int[] SortedMap(TElement[] buffer, int minIdx, int maxIdx) => + GetEnumerableSorter().Sort(buffer, buffer.Length, minIdx, maxIdx); public virtual IEnumerator GetEnumerator() { - Buffer buffer = new Buffer(_source); - if (buffer._count > 0) + TElement[] buffer = _source.ToArray(); + if (buffer.Length > 0) { int[] map = SortedMap(buffer); - for (int i = 0; i < buffer._count; i++) + for (int i = 0; i < buffer.Length; i++) { - yield return buffer._items[map[i]]; + yield return buffer[map[i]]; } } } internal IEnumerator GetEnumerator(int minIdx, int maxIdx) { - Buffer buffer = new Buffer(_source); - int count = buffer._count; + TElement[] buffer = _source.ToArray(); + int count = buffer.Length; if (count > minIdx) { if (count <= maxIdx) @@ -44,14 +44,14 @@ internal IEnumerator GetEnumerator(int minIdx, int maxIdx) if (minIdx == maxIdx) { - yield return GetEnumerableSorter().ElementAt(buffer._items, count, minIdx); + yield return GetEnumerableSorter().ElementAt(buffer, count, minIdx); } else { int[] map = SortedMap(buffer, minIdx, maxIdx); while (minIdx <= maxIdx) { - yield return buffer._items[map[minIdx]]; + yield return buffer[map[minIdx]]; ++minIdx; } } @@ -184,13 +184,13 @@ internal override EnumerableSorter GetEnumerableSorter(EnumerableSorte public override IEnumerator GetEnumerator() { - var buffer = new Buffer(_source); - if (buffer._count > 0) + TElement[] buffer = _source.ToArray(); + if (buffer.Length > 0) { - Sort(buffer._items.AsSpan(0, buffer._count), _descending); - for (int i = 0; i < buffer._count; i++) + Sort(buffer, _descending); + for (int i = 0; i < buffer.Length; i++) { - yield return buffer._items[i]; + yield return buffer[i]; } } } diff --git a/src/libraries/System.Linq/src/System/Linq/Partition.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/Partition.SpeedOpt.cs index 908ece34831c22..7bbcd298e877be 100644 --- a/src/libraries/System.Linq/src/System/Linq/Partition.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Partition.SpeedOpt.cs @@ -609,9 +609,8 @@ public TSource[] ToArray() int remaining = Limit - 1; // Max number of items left, not counting the current element. int comparand = HasLimit ? 0 : int.MinValue; // If we don't have an upper bound, have the comparison always return true. - int maxCapacity = HasLimit ? Limit : int.MaxValue; - var builder = new LargeArrayBuilder(maxCapacity); - + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); do { remaining--; @@ -619,7 +618,10 @@ public TSource[] ToArray() } while (remaining >= comparand && en.MoveNext()); - return builder.ToArray(); + TSource[] result = builder.ToArray(); + builder.Dispose(); + + return result; } } diff --git a/src/libraries/System.Linq/src/System/Linq/Reverse.cs b/src/libraries/System.Linq/src/System/Linq/Reverse.cs index c369e6b70f37c9..773e92d6cab667 100644 --- a/src/libraries/System.Linq/src/System/Linq/Reverse.cs +++ b/src/libraries/System.Linq/src/System/Linq/Reverse.cs @@ -54,9 +54,9 @@ public override bool MoveNext() // Iteration has just started. Capture the source into an array and set _state to 2 + the count. // Having an extra field for the count would be more readable, but we save it into _state with a // bias instead to minimize field size of the iterator. - Buffer buffer = new Buffer(_source); - _buffer = buffer._items; - _state = buffer._count + 2; + TSource[] buffer = _source.ToArray(); + _buffer = buffer; + _state = buffer.Length + 2; goto default; default: // At this stage, _state starts from 2 + the count. _state - 3 represents the current index into the diff --git a/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs b/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs new file mode 100644 index 00000000000000..ef85efa11ee48d --- /dev/null +++ b/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs @@ -0,0 +1,313 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace System.Collections.Generic +{ + /// Provides a helper for efficiently building arrays and lists. + /// This is implemented as an inline array of rented arrays. + /// Specifies the element type of the collection being built. + internal ref struct SegmentedArrayBuilder + { + /// The size to use for the first segment that's stack allocated by the caller. + /// + /// This value needs to be small enough that we don't need to be overly concerned about really large + /// value types. It's not unreasonable for a method to say it has 8 locals of a T, and that's effectively + /// what this is. + /// + private const int ScratchBufferSize = 8; + /// Minimum size to request renting from the pool. + private const int MinimumRentSize = 16; + + /// The array of segments. + /// is how many of the segments are valid in , not including . + private Arrays _segments; + /// The scratch buffer provided by the caller. + /// This is treated as the initial segment, before anything in . + private Span _firstSegment; + /// The current span. This points either to or to [ - 1]. + private Span _currentSegment; + /// The count of segments in that are valid. + /// All but the last are known to be fully populated. + private int _segmentsCount; + /// The total number of elements in all but the current/last segment. + private int _countInFinishedSegments; + /// The number of elements in the current/last segment. + private int _countInCurrentSegment; + + /// Initialize the builder. + /// A buffer that can be used as part of the builder. + public SegmentedArrayBuilder(Span scratchBuffer) + { + _currentSegment = _firstSegment = scratchBuffer; + } + + /// Clean up the resources used by the builder. + public void Dispose() + { + int segmentsCount = _segmentsCount; + if (segmentsCount != 0) + { + ReadOnlySpan segments = _segments; + + // We need to return all rented arrays to the pool, and if the arrays contain any references, + // we want to clear them first so that the pool doesn't artificially root contained objects. + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + // Return all but the last segment. All of these are full and need to be entirely cleared. + foreach (T[] segment in segments.Slice(0, segmentsCount - 1)) + { + ArrayPool.Shared.Return(segment, clearArray: true); + } + + // For the last segment, we can clear only what we know was used. + T[] currentSegment = segments[segmentsCount - 1]; + Array.Clear(currentSegment, 0, _countInCurrentSegment); + ArrayPool.Shared.Return(currentSegment); + } + else + { + // Return every rented array without clearing. + foreach (T[] segment in segments.Slice(0, segmentsCount)) + { + ArrayPool.Shared.Return(segment); + } + } + } + } + + /// Gets the number of elements in the builder. + public readonly int Count => checked(_countInFinishedSegments + _countInCurrentSegment); + + /// Adds an item into the builder. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + Span currentSegment = _currentSegment; + int countInCurrentSegment = _countInCurrentSegment; + if ((uint)countInCurrentSegment < (uint)currentSegment.Length) + { + currentSegment[countInCurrentSegment] = item; + _countInCurrentSegment++; + } + else + { + AddSlow(item); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddSlow(T item) + { + Expand(); + _currentSegment[0] = item; + _countInCurrentSegment = 1; + } + + /// Adds a collection of items into the builder. + public void AddRange(IEnumerable source) + { + if (source is ICollection collection) + { + int collectionCount = collection.Count; + + // If the source is empty, there's nothing to add. + if (collectionCount == 0) + { + return; + } + + // If the source is something from which we can get a span, e.g. a T[] or a List, + // we can do one or two copies to handle copying everything, even if we need to split + // across segments. + if (Enumerable.TryGetSpan(source, out ReadOnlySpan sourceSpan)) + { + int availableSpaceInCurrentSpan = _currentSegment.Length - _countInCurrentSegment; + ReadOnlySpan sourceSlice = sourceSpan.Slice(0, Math.Min(availableSpaceInCurrentSpan, sourceSpan.Length)); + sourceSlice.CopyTo(_currentSegment.Slice(_countInCurrentSegment)); + _countInCurrentSegment += sourceSlice.Length; + sourceSlice = sourceSpan.Slice(sourceSlice.Length); + + if (!sourceSlice.IsEmpty) + { + Expand(sourceSlice.Length); + sourceSlice.CopyTo(_currentSegment); + _countInCurrentSegment = sourceSlice.Length; + } + + return; + } + + // Otherwise, since we have an ICollection, we'd like to use ICollection.CopyTo, but it + // requires targeting an array, so we can't use it if we're using a scratch buffer. + bool currentSegmentIsScratchBufferWithRemainingSpace = _segmentsCount == 0 && _countInCurrentSegment < _currentSegment.Length; + if (!currentSegmentIsScratchBufferWithRemainingSpace) + { + // It also only works if we can copy the whole collection in one go, which + // means we need enough space in the current segment to hold the whole collection + // or we need to be at the end of the current segment so we can allocate a + // new one without leaving any holes. + int remainingSpaceInCurrentSegment = _currentSegment.Length - _countInCurrentSegment; + + // If there's no space remaining in the current segment, we can just expand + // into a new segment that we ensure is large enough. + if (remainingSpaceInCurrentSegment == 0) + { + Expand(collectionCount); + collection.CopyTo(_segments[_segmentsCount - 1], 0); + _countInCurrentSegment = collectionCount; + return; + } + + // If there's enough space remaining in the current segment, we can also just copy into it. + if (collectionCount <= remainingSpaceInCurrentSegment) + { + collection.CopyTo(_segments[_segmentsCount - 1], _countInCurrentSegment); + _countInCurrentSegment += collectionCount; + return; + } + + // Otherwise, we're forced to fall back to enumeration. + } + } + + // Fall back to enumerating and adding each element individually. + AddNonICollectionRangeInlined(source); + } + + /// Adds a collection of items into the builder. + /// + /// The implementation assumes the caller has already ruled out the source being + /// and ICollection and thus doesn't bother checking to see if it is. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public void AddNonICollectionRange(IEnumerable source) => + AddNonICollectionRangeInlined(source); + + /// Adds a collection of items into the builder. + /// + /// The implementation assumes the caller has already ruled out the source being + /// and ICollection and thus doesn't bother checking to see if it is. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddNonICollectionRangeInlined(IEnumerable source) + { + Span currentSegment = _currentSegment; + int countInCurrentSegment = _countInCurrentSegment; + + foreach (T item in source) + { + if ((uint)countInCurrentSegment < (uint)currentSegment.Length) + { + currentSegment[countInCurrentSegment] = item; + countInCurrentSegment++; + } + else + { + Expand(); + currentSegment = _currentSegment; + currentSegment[0] = item; + countInCurrentSegment = 1; + } + } + + _countInCurrentSegment = countInCurrentSegment; + } + + /// Creates an array containing all of the elements in the builder. + /// The number of extra elements of room to allocate in the resulting array. + public T[] ToArray(int additionalLength = 0) + { + T[] result = []; + + int count = checked(Count + additionalLength); + if (count != 0) + { + result = GC.AllocateUninitializedArray(count); + ToSpan(result); + } + + return result; + } + + /// Populates the destination span with all of the elements in the builder. + /// The destination span. + public void ToSpan(Span destination) + { + int segmentsCount = _segmentsCount; + if (segmentsCount != 0) + { + // Copy the first segment + ReadOnlySpan firstSegment = _firstSegment; + firstSegment.CopyTo(destination); + destination = destination.Slice(firstSegment.Length); + + // Copy the 0..N-1 segments + segmentsCount--; + foreach (T[] arr in ((ReadOnlySpan)_segments).Slice(0, segmentsCount)) + { + ReadOnlySpan segment = arr; + segment.CopyTo(destination); + destination = destination.Slice(segment.Length); + } + } + + // Copy the last segment + _currentSegment.Slice(0, _countInCurrentSegment).CopyTo(destination); + } + + /// Appends a new segment onto the builder. + /// The minimum amount of space to allocate in a new segment being appended. + private void Expand(int minimumRequired = MinimumRentSize) + { + if (minimumRequired < MinimumRentSize) + { + minimumRequired = MinimumRentSize; + } + + // Update our count of the number of elements in the arrays. + // If we know we're exceeding the maximum allowed array length, throw. + int currentSegmentLength = _currentSegment.Length; + checked { _countInFinishedSegments += currentSegmentLength; } + if (_countInFinishedSegments > Array.MaxLength) + { + throw new OutOfMemoryException(); + } + + // Use a typical doubling algorithm to decide the length of the next array + // and allocate it. We want to double the current array length, but if the + // minimum required is larger than that, use the minimum required. And if + // doubling would result in going above the max array length, only use the + // max array length, as List does. + int newSegmentLength = (int)Math.Min(Math.Max(minimumRequired, currentSegmentLength * 2L), Array.MaxLength); + _currentSegment = _segments[_segmentsCount] = ArrayPool.Shared.Rent(newSegmentLength); + _segmentsCount++; + } + +#pragma warning disable IDE0044 // Add readonly modifier +#pragma warning disable IDE0051 // Remove unused private members + /// A struct to hold all of the T[]s that compose the full result set. + /// + /// Starting at the minimum size of , and with a minimum of doubling + /// on every growth, this is large enough to hold the maximum number arrays that could result + /// until the total length has exceeded Array.MaxLength. + /// + [InlineArray(27)] + private struct Arrays + { + private T[] _values; + } + + /// Provides a stack-allocatable buffer for use as an argument to the builder. + [InlineArray(ScratchBufferSize)] + public struct ScratchBuffer + { + private T _item; + } +#pragma warning restore IDE0051 +#pragma warning restore IDE0044 + } +} diff --git a/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs index faf09335d5f3a3..f1c7e8e2f7ac21 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs @@ -20,23 +20,29 @@ private sealed partial class SelectEnumerableIterator : IIList { public TResult[] ToArray() { - LargeArrayBuilder builder = new(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + Func selector = _selector; foreach (TSource item in _source) { - builder.Add(_selector(item)); + builder.Add(selector(item)); } - return builder.ToArray(); + TResult[] result = builder.ToArray(); + builder.Dispose(); + + return result; } public List ToList() { var list = new List(); + Func selector = _selector; foreach (TSource item in _source) { - list.Add(_selector(item)); + list.Add(selector(item)); } return list; @@ -596,13 +602,19 @@ private TResult[] LazyToArray() { Debug.Assert(_source.GetCount(onlyIfCheap: true) == -1); - LargeArrayBuilder builder = new(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + Func selector = _selector; foreach (TSource input in _source) { - builder.Add(_selector(input)); + builder.Add(selector(input)); } - return builder.ToArray(); + + TResult[] result = builder.ToArray(); + builder.Dispose(); + + return result; } private TResult[] PreallocatingToArray(int count) diff --git a/src/libraries/System.Linq/src/System/Linq/SelectMany.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/SelectMany.SpeedOpt.cs index b485700db4dd2c..050ae6a4e06b61 100644 --- a/src/libraries/System.Linq/src/System/Linq/SelectMany.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/SelectMany.SpeedOpt.cs @@ -31,39 +31,29 @@ public int GetCount(bool onlyIfCheap) public TResult[] ToArray() { - SparseArrayBuilder builder = new(); - ArrayBuilder> deferredCopies = default; + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); - foreach (TSource element in _source) + Func> selector = _selector; + foreach (TSource item in _source) { - IEnumerable enumerable = _selector(element); - - if (builder.ReserveOrAdd(enumerable)) - { - deferredCopies.Add(enumerable); - } + builder.AddRange(selector(item)); } - TResult[] array = builder.ToArray(); - - ArrayBuilder markers = builder.Markers; - for (int i = 0; i < markers.Count; i++) - { - Marker marker = markers[i]; - IEnumerable enumerable = deferredCopies[i]; - EnumerableHelpers.Copy(enumerable, array, marker.Index, marker.Count); - } + TResult[] result = builder.ToArray(); + builder.Dispose(); - return array; + return result; } public List ToList() { var list = new List(); + Func> selector = _selector; foreach (TSource element in _source) { - list.AddRange(_selector(element)); + list.AddRange(selector(element)); } return list; diff --git a/src/libraries/System.Linq/src/System/Linq/SingleLinkedNode.cs b/src/libraries/System.Linq/src/System/Linq/SingleLinkedNode.cs index 051ebe769c1f25..e0bf5e54c51ee0 100644 --- a/src/libraries/System.Linq/src/System/Linq/SingleLinkedNode.cs +++ b/src/libraries/System.Linq/src/System/Linq/SingleLinkedNode.cs @@ -99,7 +99,7 @@ public TSource[] ToArray(int count) /// /// Fills a start of a span with the items of this node's singly-linked list. /// - /// The span to fill. Must be the precise size required. + /// The span to fill. Must be at least the size required. public void Fill(Span span) { int index = 0; diff --git a/src/libraries/System.Linq/src/System/Linq/Sum.cs b/src/libraries/System.Linq/src/System/Linq/Sum.cs index ae670ff3e96876..7eb2dc855dc67b 100644 --- a/src/libraries/System.Linq/src/System/Linq/Sum.cs +++ b/src/libraries/System.Linq/src/System/Linq/Sum.cs @@ -25,6 +25,11 @@ private static TResult Sum(this IEnumerable source) where TSource : struct, INumber where TResult : struct, INumber { + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + if (source.TryGetSpan(out ReadOnlySpan span)) { return Sum(span); diff --git a/src/libraries/System.Linq/src/System/Linq/ToCollection.cs b/src/libraries/System.Linq/src/System/Linq/ToCollection.cs index de145c6dccfb19..ba43357049e70b 100644 --- a/src/libraries/System.Linq/src/System/Linq/ToCollection.cs +++ b/src/libraries/System.Linq/src/System/Linq/ToCollection.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; namespace System.Linq { @@ -10,24 +9,54 @@ public static partial class Enumerable { public static TSource[] ToArray(this IEnumerable source) { - if (source == null) + if (source is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } - return source is IIListProvider arrayProvider - ? arrayProvider.ToArray() - : EnumerableHelpers.ToArray(source); + if (source is IIListProvider arrayProvider) + { + return arrayProvider.ToArray(); + } + + if (source is ICollection collection) + { + int count = collection.Count; + if (count != 0) + { + var result = new TSource[count]; + collection.CopyTo(result, 0); + return result; + } + + return []; + } + else + { + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + + builder.AddNonICollectionRange(source); + TSource[] result = builder.ToArray(); + + builder.Dispose(); + return result; + } } public static List ToList(this IEnumerable source) { - if (source == null) + if (source is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } - return source is IIListProvider listProvider ? listProvider.ToList() : new List(source); + if (source is IIListProvider listProvider) + { + return listProvider.ToList(); + } + + return new List(source); } /// @@ -98,12 +127,12 @@ public static Dictionary ToDictionary(this IEnumer public static Dictionary ToDictionary(this IEnumerable source, Func keySelector, IEqualityComparer? comparer) where TKey : notnull { - if (source == null) + if (source is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } - if (keySelector == null) + if (keySelector is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.keySelector); } @@ -164,17 +193,17 @@ public static Dictionary ToDictionary(t public static Dictionary ToDictionary(this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer? comparer) where TKey : notnull { - if (source == null) + if (source is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } - if (keySelector == null) + if (keySelector is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.keySelector); } - if (elementSelector == null) + if (elementSelector is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.elementSelector); } @@ -234,7 +263,7 @@ private static Dictionary ToDictionary( public static HashSet ToHashSet(this IEnumerable source, IEqualityComparer? comparer) { - if (source == null) + if (source is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } diff --git a/src/libraries/System.Linq/src/System/Linq/Utilities.cs b/src/libraries/System.Linq/src/System/Linq/Utilities.cs index 2f0aeef28c6d11..208d6878040a48 100644 --- a/src/libraries/System.Linq/src/System/Linq/Utilities.cs +++ b/src/libraries/System.Linq/src/System/Linq/Utilities.cs @@ -6,7 +6,7 @@ namespace System.Linq { /// - /// Contains helper methods for System.Linq. Please put enumerable-related methods in . + /// Contains helper methods for System.Linq. /// internal static class Utilities { diff --git a/src/libraries/System.Linq/src/System/Linq/Where.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/Where.SpeedOpt.cs index 858b3b62ab9ba3..ccea8233be9c9b 100644 --- a/src/libraries/System.Linq/src/System/Linq/Where.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Where.SpeedOpt.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Runtime.InteropServices; namespace System.Linq { @@ -34,26 +35,32 @@ public int GetCount(bool onlyIfCheap) public TSource[] ToArray() { - LargeArrayBuilder builder = new(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + Func predicate = _predicate; foreach (TSource item in _source) { - if (_predicate(item)) + if (predicate(item)) { builder.Add(item); } } - return builder.ToArray(); + TSource[] result = builder.ToArray(); + builder.Dispose(); + + return result; } public List ToList() { var list = new List(); + Func predicate = _predicate; foreach (TSource item in _source) { - if (_predicate(item)) + if (predicate(item)) { list.Add(item); } @@ -65,8 +72,13 @@ public List ToList() internal sealed partial class WhereArrayIterator : IIListProvider { - public int GetCount(bool onlyIfCheap) + public int GetCount(bool onlyIfCheap) => GetCount(onlyIfCheap, _source, _predicate); + + public static int GetCount(bool onlyIfCheap, ReadOnlySpan source, Func predicate) { + // In case someone uses Count() to force evaluation of + // the selector, run it provided `onlyIfCheap` is false. + if (onlyIfCheap) { return -1; @@ -74,42 +86,46 @@ public int GetCount(bool onlyIfCheap) int count = 0; - foreach (TSource item in _source) + foreach (TSource item in source) { - if (_predicate(item)) + if (predicate(item)) { - checked - { - count++; - } + checked { count++; } } } return count; } - public TSource[] ToArray() + public TSource[] ToArray() => ToArray(_source, _predicate); + + public static TSource[] ToArray(ReadOnlySpan source, Func predicate) { - var builder = new LargeArrayBuilder(_source.Length); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); - foreach (TSource item in _source) + foreach (TSource item in source) { - if (_predicate(item)) + if (predicate(item)) { builder.Add(item); } } - return builder.ToArray(); + TSource[] result = builder.ToArray(); + builder.Dispose(); + + return result; } public List ToList() { var list = new List(); + Func predicate = _predicate; foreach (TSource item in _source) { - if (_predicate(item)) + if (predicate(item)) { list.Add(item); } @@ -121,54 +137,18 @@ public List ToList() private sealed partial class WhereListIterator : Iterator, IIListProvider { - public int GetCount(bool onlyIfCheap) - { - if (onlyIfCheap) - { - return -1; - } + public int GetCount(bool onlyIfCheap) => WhereArrayIterator.GetCount(onlyIfCheap, CollectionsMarshal.AsSpan(_source), _predicate); - int count = 0; - - for (int i = 0; i < _source.Count; i++) - { - TSource item = _source[i]; - if (_predicate(item)) - { - checked - { - count++; - } - } - } - - return count; - } - - public TSource[] ToArray() - { - var builder = new LargeArrayBuilder(_source.Count); - - for (int i = 0; i < _source.Count; i++) - { - TSource item = _source[i]; - if (_predicate(item)) - { - builder.Add(item); - } - } - - return builder.ToArray(); - } + public TSource[] ToArray() => WhereArrayIterator.ToArray(CollectionsMarshal.AsSpan(_source), _predicate); public List ToList() { var list = new List(); - for (int i = 0; i < _source.Count; i++) + Func predicate = _predicate; + foreach (TSource item in CollectionsMarshal.AsSpan(_source)) { - TSource item = _source[i]; - if (_predicate(item)) + if (predicate(item)) { list.Add(item); } @@ -180,7 +160,9 @@ public List ToList() private sealed partial class WhereSelectArrayIterator : IIListProvider { - public int GetCount(bool onlyIfCheap) + public int GetCount(bool onlyIfCheap) => GetCount(onlyIfCheap, _source, _predicate, _selector); + + public static int GetCount(bool onlyIfCheap, ReadOnlySpan source, Func predicate, Func selector) { // In case someone uses Count() to force evaluation of // the selector, run it provided `onlyIfCheap` is false. @@ -192,11 +174,11 @@ public int GetCount(bool onlyIfCheap) int count = 0; - foreach (TSource item in _source) + foreach (TSource item in source) { - if (_predicate(item)) + if (predicate(item)) { - _selector(item); + selector(item); checked { count++; @@ -207,28 +189,35 @@ public int GetCount(bool onlyIfCheap) return count; } - public TResult[] ToArray() + public TResult[] ToArray() => ToArray(_source, _predicate, _selector); + + public static TResult[] ToArray(ReadOnlySpan source, Func predicate, Func selector) { - var builder = new LargeArrayBuilder(_source.Length); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); - foreach (TSource item in _source) + foreach (TSource item in source) { - if (_predicate(item)) + if (predicate(item)) { - builder.Add(_selector(item)); + builder.Add(selector(item)); } } - return builder.ToArray(); + TResult[] result = builder.ToArray(); + builder.Dispose(); + + return result; } public List ToList() { var list = new List(); + Func predicate = _predicate; foreach (TSource item in _source) { - if (_predicate(item)) + if (predicate(item)) { list.Add(_selector(item)); } @@ -240,58 +229,18 @@ public List ToList() private sealed partial class WhereSelectListIterator : IIListProvider { - public 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; - - for (int i = 0; i < _source.Count; i++) - { - TSource item = _source[i]; - if (_predicate(item)) - { - _selector(item); - checked - { - count++; - } - } - } - - return count; - } - - public TResult[] ToArray() - { - var builder = new LargeArrayBuilder(_source.Count); + public int GetCount(bool onlyIfCheap) => WhereSelectArrayIterator.GetCount(onlyIfCheap, CollectionsMarshal.AsSpan(_source), _predicate, _selector); - for (int i = 0; i < _source.Count; i++) - { - TSource item = _source[i]; - if (_predicate(item)) - { - builder.Add(_selector(item)); - } - } - - return builder.ToArray(); - } + public TResult[] ToArray() => WhereSelectArrayIterator.ToArray(CollectionsMarshal.AsSpan(_source), _predicate, _selector); public List ToList() { var list = new List(); - for (int i = 0; i < _source.Count; i++) + Func predicate = _predicate; + foreach (TSource item in CollectionsMarshal.AsSpan(_source)) { - TSource item = _source[i]; - if (_predicate(item)) + if (predicate(item)) { list.Add(_selector(item)); } @@ -332,28 +281,36 @@ public int GetCount(bool onlyIfCheap) public TResult[] ToArray() { - LargeArrayBuilder builder = new(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + Func predicate = _predicate; + Func selector = _selector; foreach (TSource item in _source) { - if (_predicate(item)) + if (predicate(item)) { - builder.Add(_selector(item)); + builder.Add(selector(item)); } } - return builder.ToArray(); + TResult[] result = builder.ToArray(); + builder.Dispose(); + + return result; } public List ToList() { var list = new List(); + Func predicate = _predicate; + Func selector = _selector; foreach (TSource item in _source) { - if (_predicate(item)) + if (predicate(item)) { - list.Add(_selector(item)); + list.Add(selector(item)); } } diff --git a/src/libraries/System.Linq/tests/LifecycleTests.cs b/src/libraries/System.Linq/tests/LifecycleTests.cs index 1331c8223a847f..bafc26e7edd03b 100644 --- a/src/libraries/System.Linq/tests/LifecycleTests.cs +++ b/src/libraries/System.Linq/tests/LifecycleTests.cs @@ -21,7 +21,7 @@ from unary2 in UnaryOperations() from sink in Sinks() select (source, unary1, unary2, sink); - Assert.All(inputs, input => + foreach (var input in inputs) { var (source, unary1, unary2, sink) = input; var e = new LifecycleTrackingEnumerable(source.Work); @@ -43,7 +43,7 @@ from sink in Sinks() bool shortCircuits = argError || ShortCircuits(source, unary1, unary2, sink); Assert.InRange(e.EnumeratorCtorCalls, shortCircuits ? 0 : 1, 1); Assert.Equal(e.EnumeratorCtorCalls, e.EnumeratorDisposeCalls); - }); + } } [Fact] @@ -57,7 +57,7 @@ from binary in BinaryOperations() from sink in Sinks() select (source, unary, binary, sink); - Assert.All(inputs, input => + foreach (var input in inputs) { var (source, unary, binary, sink) = input; var es = new[] { new LifecycleTrackingEnumerable(source.Work), new LifecycleTrackingEnumerable(source.Work) }; @@ -82,7 +82,7 @@ from sink in Sinks() Assert.InRange(e.EnumeratorCtorCalls, shortCircuits ? 0 : 1, 1); Assert.Equal(e.EnumeratorCtorCalls, e.EnumeratorDisposeCalls); }); - }); + } } private static bool ShortCircuits(params Operation[] ops) => ops.Any(o => o.ShortCircuits); diff --git a/src/libraries/System.Linq/tests/ToArrayTests.cs b/src/libraries/System.Linq/tests/ToArrayTests.cs index b4877785112a74..bc1c47ffcf91a9 100644 --- a/src/libraries/System.Linq/tests/ToArrayTests.cs +++ b/src/libraries/System.Linq/tests/ToArrayTests.cs @@ -132,7 +132,10 @@ public void ToArray_FailOnExtremelyLargeCollection() { var largeSeq = new FastInfiniteEnumerator(); var thrownException = Assert.ThrowsAny(() => { largeSeq.ToArray(); }); - Assert.True(thrownException.GetType() == typeof(OverflowException) || thrownException.GetType() == typeof(OutOfMemoryException)); + Assert.True( + thrownException.GetType() == typeof(OverflowException) || + thrownException.GetType() == typeof(OutOfMemoryException), + $"Expected OverflowException or OutOfMemoryException, got {thrownException}"); } [Theory]