Skip to content

Commit 02f311f

Browse files
committed
Make ArrayBuilder stack allocate the first 16 entries for less heap allocations
1 parent 7566217 commit 02f311f

File tree

1 file changed

+45
-26
lines changed

1 file changed

+45
-26
lines changed

src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Runtime.CompilerServices;
56

67
namespace System.Collections.Generic
78
{
@@ -11,7 +12,9 @@ namespace System.Collections.Generic
1112
/// <typeparam name="T">The element type.</typeparam>
1213
internal struct ArrayBuilder<T>
1314
{
14-
private const int DefaultCapacity = 4;
15+
private InlineArray16<T> _stackAllocatedBuffer = default;
16+
private const int StackAllocatedCapacity = 16;
17+
private const int DefaultHeapCapacity = 4;
1518

1619
private T[]? _array; // Starts out null, initialized on first Add.
1720
private int _count; // Number of items into _array we're using.
@@ -23,20 +26,17 @@ internal struct ArrayBuilder<T>
2326
public ArrayBuilder(int capacity) : this()
2427
{
2528
Debug.Assert(capacity >= 0);
26-
if (capacity > 0)
29+
if (capacity > StackAllocatedCapacity)
2730
{
28-
_array = new T[capacity];
31+
_array = new T[capacity - StackAllocatedCapacity];
2932
}
3033
}
3134

3235
/// <summary>
3336
/// Gets the number of items this instance can store without re-allocating,
3437
/// or 0 if the backing array is <c>null</c>.
3538
/// </summary>
36-
public int Capacity => _array?.Length ?? 0;
37-
38-
/// <summary>Gets the current underlying array.</summary>
39-
public T[]? Buffer => _array;
39+
public int Capacity => _array?.Length + StackAllocatedCapacity ?? StackAllocatedCapacity;
4040

4141
/// <summary>
4242
/// Gets the number of items in the array currently in use.
@@ -52,7 +52,7 @@ public T this[int index]
5252
get
5353
{
5454
Debug.Assert(index >= 0 && index < _count);
55-
return _array![index];
55+
return index < StackAllocatedCapacity ? _stackAllocatedBuffer[index] : _array![index - StackAllocatedCapacity];
5656
}
5757
}
5858

@@ -76,7 +76,7 @@ public void Add(T item)
7676
public T First()
7777
{
7878
Debug.Assert(_count > 0);
79-
return _array![0];
79+
return _stackAllocatedBuffer[0];
8080
}
8181

8282
/// <summary>
@@ -85,7 +85,7 @@ public T First()
8585
public T Last()
8686
{
8787
Debug.Assert(_count > 0);
88-
return _array![_count - 1];
88+
return _count <= StackAllocatedCapacity ? _stackAllocatedBuffer[_count - 1] : _array![_count - StackAllocatedCapacity - 1];
8989
}
9090

9191
/// <summary>
@@ -101,17 +101,19 @@ public T[] ToArray()
101101
return Array.Empty<T>();
102102
}
103103

104-
Debug.Assert(_array != null); // Nonzero _count should imply this
105-
106-
T[] result = _array;
107-
if (_count < result.Length)
104+
T[] result = new T[_count];
105+
int index = 0;
106+
foreach (T stackAllocatedValue in _stackAllocatedBuffer)
108107
{
109-
// Avoid a bit of overhead (method call, some branches, extra codegen)
110-
// which would be incurred by using Array.Resize
111-
result = new T[_count];
112-
Array.Copy(_array, result, _count);
108+
result[index++] = stackAllocatedValue;
109+
if (index >= _count)
110+
{
111+
return result;
112+
}
113113
}
114114

115+
_array.AsSpan(0, _count - StackAllocatedCapacity).CopyTo(result.AsSpan(start: StackAllocatedCapacity));
116+
115117
#if DEBUG
116118
// Try to prevent callers from using the ArrayBuilder after ToArray, if _count != 0.
117119
_count = -1;
@@ -132,25 +134,42 @@ public T[] ToArray()
132134
public void UncheckedAdd(T item)
133135
{
134136
Debug.Assert(_count < Capacity);
135-
136-
_array![_count++] = item;
137+
if (_count < StackAllocatedCapacity)
138+
{
139+
_stackAllocatedBuffer[_count++] = item;
140+
}
141+
else
142+
{
143+
_array![_count++ - StackAllocatedCapacity] = item;
144+
}
137145
}
138146

139147
private void EnsureCapacity(int minimum)
140148
{
141149
Debug.Assert(minimum > Capacity);
142150

143-
int capacity = Capacity;
144-
int nextCapacity = capacity == 0 ? DefaultCapacity : 2 * capacity;
151+
if (minimum < StackAllocatedCapacity)
152+
{
153+
return;
154+
}
155+
156+
if (_array == null)
157+
{
158+
// Initial capacity has not been set correctly, we will use the default size
159+
_array = new T[DefaultHeapCapacity];
160+
return;
161+
}
162+
163+
int nextHeapCapacity = 2 * _array.Length;
145164

146-
if ((uint)nextCapacity > (uint)Array.MaxLength)
165+
if ((uint)nextHeapCapacity > (uint)Array.MaxLength)
147166
{
148-
nextCapacity = Math.Max(capacity + 1, Array.MaxLength);
167+
nextHeapCapacity = Math.Max(_array.Length + 1, Array.MaxLength);
149168
}
150169

151-
nextCapacity = Math.Max(nextCapacity, minimum);
170+
nextHeapCapacity = Math.Max(nextHeapCapacity, minimum);
152171

153-
T[] next = new T[nextCapacity];
172+
T[] next = new T[nextHeapCapacity];
154173
if (_count > 0)
155174
{
156175
Array.Copy(_array!, next, _count);

0 commit comments

Comments
 (0)