Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak Enumerable.ToArray to reduce some overheads #97458

Merged
merged 4 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
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)
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading