From f393fdcd553bdcbaa5a346700b99c5e3acad552b Mon Sep 17 00:00:00 2001 From: neuecc Date: Sat, 12 Aug 2023 19:16:27 +0900 Subject: [PATCH] improve Enumerable.ToArray performance use InlineArray byte[] sequence instead of LargeArrayBuilder --- .../Generic/EnumerableHelpers.Linq.cs | 111 +++++++++++++++++- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs b/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs index 13970c6a0fc22b..c595168117058f 100644 --- a/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs +++ b/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Linq; +using System.Runtime.CompilerServices; namespace System.Collections.Generic { @@ -80,9 +80,112 @@ internal static T[] ToArray(IEnumerable source) return result; } - LargeArrayBuilder builder = new(); - builder.AddRange(source); - return builder.ToArray(); + ToArrayHelper helper = new ToArrayHelper(initialCapacity: 4); + using (IEnumerator e = source.GetEnumerator()) + { + while (true) + { + Span span = helper.CurrentSpan; + for (int i = 0; i < span.Length; i++) + { + if (e.MoveNext()) + { + span[i] = e.Current; + } + else + { + return helper.ToArray(i); + } + } + helper.AllocateNextBlock(); + } + } + } + + // when smallest block size is 4, reaching Array.MaxLength size is "29". + // length, blockSize, totalSize + // 1 4 4 + // 2 8 12 + // 3 16 28 + // 4 32 60 + // 5 64 124 + // 6 128 252 + // 7 256 508 + // 8 512 1020 + // 9 1024 2044 + // 10 2048 4092 + // 11 4096 8188 + // 12 8192 16380 + // 13 16384 32764 + // 14 32768 65532 + // 15 65536 131068 + // 16 131072 262140 + // 17 262144 524284 + // 18 524288 1048572 + // 19 1048576 2097148 + // 20 2097152 4194300 + // 21 4194304 8388604 + // 22 8388608 16777212 + // 23 16777216 33554428 + // 24 33554432 67108860 + // 25 67108864 134217724 + // 26 134217728 268435452 + // 27 268435456 536870908 + // 28 536870912 1073741820 + // 29 1073741771 2147483591 <- reach Array.MaxLength(2147483591) + [InlineArray(29)] + private struct ArrayBlock + { +#pragma warning disable CA1823 // Avoid unused private fields +#pragma warning disable IDE0044 // Add readonly modifier +#pragma warning disable IDE0051 // Remove unused private members + private T[] _array; +#pragma warning restore IDE0051 // Remove unused private members +#pragma warning restore IDE0044 // Add readonly modifier +#pragma warning restore CA1823 // Avoid unused private fields + } + + private struct ToArrayHelper + { + private int _index; + private int _count; + private T[] _currentBlock; + private ArrayBlock _blocks; + + public ToArrayHelper(int initialCapacity) + { + _blocks = default(ArrayBlock); + _currentBlock = _blocks[0] = new T[initialCapacity]; + } + + public Span CurrentSpan => _currentBlock; + + public void AllocateNextBlock() + { + _index++; + _count += _currentBlock.Length; + + int nextSize = unchecked(_currentBlock.Length * 2); + if (nextSize < 0 || Array.MaxLength < (_count + nextSize)) + { + nextSize = Array.MaxLength - _count; + } + + _currentBlock = _blocks[_index] = new T[nextSize]; + } + + public T[] ToArray(int lastBlockCount) + { + T[] array = GC.AllocateUninitializedArray(_count + lastBlockCount); + Span dest = array.AsSpan(); + for (int i = 0; i < _index; i++) + { + _blocks[i].CopyTo(dest); + dest = dest.Slice(_blocks[i].Length); + } + _currentBlock.AsSpan(0, lastBlockCount).CopyTo(dest); + return array; + } } } }