Skip to content

Commit

Permalink
Tweak Enumerable.ToArray to reduce some overheads (#97458)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephentoub authored Jan 27, 2024
1 parent e1f7047 commit 19d5ae1
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 31 deletions.
103 changes: 75 additions & 28 deletions src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;

Expand Down Expand Up @@ -51,30 +52,44 @@ public void Dispose()
int segmentsCount = _segmentsCount;
if (segmentsCount != 0)
{
ReadOnlySpan<T[]> segments = _segments;
ReturnArrays(segmentsCount);
}
}

// 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<T>())
{
// 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<T>.Shared.Return(segment, clearArray: true);
}
private void ReturnArrays(int segmentsCount)
{
Debug.Assert(segmentsCount > 0);
ReadOnlySpan<T[]> segments = _segments;

// For the last segment, we can clear only what we know was used.
T[] currentSegment = segments[segmentsCount - 1];
Array.Clear(currentSegment, 0, _countInCurrentSegment);
ArrayPool<T>.Shared.Return(currentSegment);
// 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<T>())
{
// Return all but the last segment. All of these are full and need to be entirely cleared.
segmentsCount--;
foreach (T[] segment in segments.Slice(0, segmentsCount))
{
Array.Clear(segment);
ArrayPool<T>.Shared.Return(segment);
}
else

// For the last segment, we can clear only what we know was used.
T[] currentSegment = segments[segmentsCount];
Array.Clear(currentSegment, 0, _countInCurrentSegment);
ArrayPool<T>.Shared.Return(currentSegment);
}
else
{
// Return every rented array without clearing.
for (int i = 0; i < segments.Length; i++)
{
// Return every rented array without clearing.
foreach (T[] segment in segments.Slice(0, segmentsCount))
T[] segment = segments[i];
if (segment is null)
{
ArrayPool<T>.Shared.Return(segment);
break;
}

ArrayPool<T>.Shared.Return(segment);
}
}
}
Expand Down Expand Up @@ -193,7 +208,7 @@ public void AddNonICollectionRange(IEnumerable<T> source) =>
/// and ICollection and thus doesn't bother checking to see if it is.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddNonICollectionRangeInlined(IEnumerable<T> source)
internal void AddNonICollectionRangeInlined(IEnumerable<T> source)
{
Span<T> currentSegment = _currentSegment;
int countInCurrentSegment = _countInCurrentSegment;
Expand All @@ -218,24 +233,53 @@ private void AddNonICollectionRangeInlined(IEnumerable<T> source)
}

/// <summary>Creates an array containing all of the elements in the builder.</summary>
/// <param name="additionalLength">The number of extra elements of room to allocate in the resulting array.</param>
public T[] ToArray(int additionalLength = 0)
public readonly T[] ToArray()
{
T[] result = [];
T[] result;
int count = Count;

if (count != 0)
{
result = GC.AllocateUninitializedArray<T>(count);
ToSpanInlined(result);
}
else
{
result = [];
}

return result;
}

/// <summary>Creates an array containing all of the elements in the builder.</summary>
/// <param name="additionalLength">The number of extra elements of room to allocate in the resulting array.</param>
public readonly T[] ToArray(int additionalLength)
{
T[] result;
int count = checked(Count + additionalLength);

if (count != 0)
{
result = GC.AllocateUninitializedArray<T>(count);
ToSpan(result);
ToSpanInlined(result);
}
else
{
result = [];
}

return result;
}

/// <summary>Populates the destination span with all of the elements in the builder.</summary>
/// <param name="destination">The destination span.</param>
public void ToSpan(Span<T> destination)
[MethodImpl(MethodImplOptions.NoInlining)]
public readonly void ToSpan(Span<T> destination) => ToSpanInlined(destination);

/// <summary>Populates the destination span with all of the elements in the builder.</summary>
/// <param name="destination">The destination span.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly void ToSpanInlined(Span<T> destination)
{
int segmentsCount = _segmentsCount;
if (segmentsCount != 0)
Expand All @@ -247,11 +291,14 @@ public void ToSpan(Span<T> destination)

// Copy the 0..N-1 segments
segmentsCount--;
foreach (T[] arr in ((ReadOnlySpan<T[]>)_segments).Slice(0, segmentsCount))
if (segmentsCount != 0)
{
ReadOnlySpan<T> segment = arr;
segment.CopyTo(destination);
destination = destination.Slice(segment.Length);
foreach (T[] arr in ((ReadOnlySpan<T[]>)_segments).Slice(0, segmentsCount))
{
ReadOnlySpan<T> segment = arr;
segment.CopyTo(destination);
destination = destination.Slice(segment.Length);
}
}
}

Expand Down
10 changes: 7 additions & 3 deletions src/libraries/System.Linq/src/System/Linq/ToCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Linq
Expand Down Expand Up @@ -33,12 +33,16 @@ public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source)

return [];
}
else

return EnumerableToArray(source);

[MethodImpl(MethodImplOptions.NoInlining)] // avoid large stack allocation impacting other paths
static TSource[] EnumerableToArray(IEnumerable<TSource> source)
{
SegmentedArrayBuilder<TSource>.ScratchBuffer scratch = default;
SegmentedArrayBuilder<TSource> builder = new(scratch);

builder.AddNonICollectionRange(source);
builder.AddNonICollectionRangeInlined(source);
TSource[] result = builder.ToArray();

builder.Dispose();
Expand Down

0 comments on commit 19d5ae1

Please sign in to comment.