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 6e810da322b40f..2fc4153be6890a 100644 --- a/src/libraries/System.Linq/src/System.Linq.csproj +++ b/src/libraries/System.Linq/src/System.Linq.csproj @@ -14,8 +14,6 @@ - @@ -36,26 +34,13 @@ - - - - - - - @@ -88,6 +73,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 1df7809baa65c6..d3e67aac26e166 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 887a3800683124..fbdf9a12a28b65 100644 --- a/src/libraries/System.Linq/src/System/Linq/Enumerable.cs +++ b/src/libraries/System.Linq/src/System/Linq/Enumerable.cs @@ -37,18 +37,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 d3189720456d73..d0da2f7bfd7415 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 759c4ce65627dd..f8af828bc95087 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 632d643655174a..3a0f2130d966b3 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 1e6f367bb37a85..b7a45d95a1a1fb 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 []; } - if (count <= maxIdx) + if (buffer.Length <= maxIdx) { - maxIdx = count - 1; + maxIdx = buffer.Length - 1; } if (minIdx == maxIdx) { - return [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 dbcd20974a3066..92f71cb058d478 100644 --- a/src/libraries/System.Linq/src/System/Linq/Partition.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Partition.SpeedOpt.cs @@ -511,9 +511,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--; @@ -521,7 +520,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 674b30bbc49c94..2a02115daca12a 100644 --- a/src/libraries/System.Linq/src/System/Linq/Reverse.cs +++ b/src/libraries/System.Linq/src/System/Linq/Reverse.cs @@ -59,9 +59,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 c9e8be3f9744f4..f59b6e7b08fbae 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; @@ -598,13 +604,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 2fd7f475fafbf4..8b2ad651d1d2bd 100644 --- a/src/libraries/System.Linq/src/System/Linq/ToCollection.cs +++ b/src/libraries/System.Linq/src/System/Linq/ToCollection.cs @@ -11,24 +11,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); } /// @@ -99,12 +129,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); } @@ -152,17 +182,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); } @@ -209,7 +239,7 @@ private static Dictionary SpanToDictionary 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]