From 02f311fb84fda550d347dc628f80ad30be3c24f8 Mon Sep 17 00:00:00 2001 From: Henrik Gedionsen Date: Mon, 6 Oct 2025 13:49:30 +0200 Subject: [PATCH 1/3] Make ArrayBuilder stack allocate the first 16 entries for less heap allocations --- .../Collections/Generic/ArrayBuilder.cs | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs index ec862f46d5c3f1..c03b598d373c79 100644 --- a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs +++ b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Runtime.CompilerServices; namespace System.Collections.Generic { @@ -11,7 +12,9 @@ namespace System.Collections.Generic /// The element type. internal struct ArrayBuilder { - private const int DefaultCapacity = 4; + private InlineArray16 _stackAllocatedBuffer = default; + private const int StackAllocatedCapacity = 16; + private const int DefaultHeapCapacity = 4; private T[]? _array; // Starts out null, initialized on first Add. private int _count; // Number of items into _array we're using. @@ -23,9 +26,9 @@ internal struct ArrayBuilder public ArrayBuilder(int capacity) : this() { Debug.Assert(capacity >= 0); - if (capacity > 0) + if (capacity > StackAllocatedCapacity) { - _array = new T[capacity]; + _array = new T[capacity - StackAllocatedCapacity]; } } @@ -33,10 +36,7 @@ public ArrayBuilder(int capacity) : this() /// Gets the number of items this instance can store without re-allocating, /// or 0 if the backing array is null. /// - public int Capacity => _array?.Length ?? 0; - - /// Gets the current underlying array. - public T[]? Buffer => _array; + public int Capacity => _array?.Length + StackAllocatedCapacity ?? StackAllocatedCapacity; /// /// Gets the number of items in the array currently in use. @@ -52,7 +52,7 @@ public T this[int index] get { Debug.Assert(index >= 0 && index < _count); - return _array![index]; + return index < StackAllocatedCapacity ? _stackAllocatedBuffer[index] : _array![index - StackAllocatedCapacity]; } } @@ -76,7 +76,7 @@ public void Add(T item) public T First() { Debug.Assert(_count > 0); - return _array![0]; + return _stackAllocatedBuffer[0]; } /// @@ -85,7 +85,7 @@ public T First() public T Last() { Debug.Assert(_count > 0); - return _array![_count - 1]; + return _count <= StackAllocatedCapacity ? _stackAllocatedBuffer[_count - 1] : _array![_count - StackAllocatedCapacity - 1]; } /// @@ -101,17 +101,19 @@ public T[] ToArray() return Array.Empty(); } - Debug.Assert(_array != null); // Nonzero _count should imply this - - T[] result = _array; - if (_count < result.Length) + T[] result = new T[_count]; + int index = 0; + foreach (T stackAllocatedValue in _stackAllocatedBuffer) { - // Avoid a bit of overhead (method call, some branches, extra codegen) - // which would be incurred by using Array.Resize - result = new T[_count]; - Array.Copy(_array, result, _count); + result[index++] = stackAllocatedValue; + if (index >= _count) + { + return result; + } } + _array.AsSpan(0, _count - StackAllocatedCapacity).CopyTo(result.AsSpan(start: StackAllocatedCapacity)); + #if DEBUG // Try to prevent callers from using the ArrayBuilder after ToArray, if _count != 0. _count = -1; @@ -132,25 +134,42 @@ public T[] ToArray() public void UncheckedAdd(T item) { Debug.Assert(_count < Capacity); - - _array![_count++] = item; + if (_count < StackAllocatedCapacity) + { + _stackAllocatedBuffer[_count++] = item; + } + else + { + _array![_count++ - StackAllocatedCapacity] = item; + } } private void EnsureCapacity(int minimum) { Debug.Assert(minimum > Capacity); - int capacity = Capacity; - int nextCapacity = capacity == 0 ? DefaultCapacity : 2 * capacity; + if (minimum < StackAllocatedCapacity) + { + return; + } + + if (_array == null) + { + // Initial capacity has not been set correctly, we will use the default size + _array = new T[DefaultHeapCapacity]; + return; + } + + int nextHeapCapacity = 2 * _array.Length; - if ((uint)nextCapacity > (uint)Array.MaxLength) + if ((uint)nextHeapCapacity > (uint)Array.MaxLength) { - nextCapacity = Math.Max(capacity + 1, Array.MaxLength); + nextHeapCapacity = Math.Max(_array.Length + 1, Array.MaxLength); } - nextCapacity = Math.Max(nextCapacity, minimum); + nextHeapCapacity = Math.Max(nextHeapCapacity, minimum); - T[] next = new T[nextCapacity]; + T[] next = new T[nextHeapCapacity]; if (_count > 0) { Array.Copy(_array!, next, _count); From 1d0e7559d487ea0f855f033c4d5ceb1622cae856 Mon Sep 17 00:00:00 2001 From: Henrik Gedionsen Date: Tue, 7 Oct 2025 00:12:40 +0200 Subject: [PATCH 2/3] Revert ArrayBuilder changes, it cannot be changed --- .../Collections/Generic/ArrayBuilder.cs | 71 +++++++------------ 1 file changed, 26 insertions(+), 45 deletions(-) diff --git a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs index c03b598d373c79..ec862f46d5c3f1 100644 --- a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs +++ b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Runtime.CompilerServices; namespace System.Collections.Generic { @@ -12,9 +11,7 @@ namespace System.Collections.Generic /// The element type. internal struct ArrayBuilder { - private InlineArray16 _stackAllocatedBuffer = default; - private const int StackAllocatedCapacity = 16; - private const int DefaultHeapCapacity = 4; + private const int DefaultCapacity = 4; private T[]? _array; // Starts out null, initialized on first Add. private int _count; // Number of items into _array we're using. @@ -26,9 +23,9 @@ internal struct ArrayBuilder public ArrayBuilder(int capacity) : this() { Debug.Assert(capacity >= 0); - if (capacity > StackAllocatedCapacity) + if (capacity > 0) { - _array = new T[capacity - StackAllocatedCapacity]; + _array = new T[capacity]; } } @@ -36,7 +33,10 @@ public ArrayBuilder(int capacity) : this() /// Gets the number of items this instance can store without re-allocating, /// or 0 if the backing array is null. /// - public int Capacity => _array?.Length + StackAllocatedCapacity ?? StackAllocatedCapacity; + public int Capacity => _array?.Length ?? 0; + + /// Gets the current underlying array. + public T[]? Buffer => _array; /// /// Gets the number of items in the array currently in use. @@ -52,7 +52,7 @@ public T this[int index] get { Debug.Assert(index >= 0 && index < _count); - return index < StackAllocatedCapacity ? _stackAllocatedBuffer[index] : _array![index - StackAllocatedCapacity]; + return _array![index]; } } @@ -76,7 +76,7 @@ public void Add(T item) public T First() { Debug.Assert(_count > 0); - return _stackAllocatedBuffer[0]; + return _array![0]; } /// @@ -85,7 +85,7 @@ public T First() public T Last() { Debug.Assert(_count > 0); - return _count <= StackAllocatedCapacity ? _stackAllocatedBuffer[_count - 1] : _array![_count - StackAllocatedCapacity - 1]; + return _array![_count - 1]; } /// @@ -101,19 +101,17 @@ public T[] ToArray() return Array.Empty(); } - T[] result = new T[_count]; - int index = 0; - foreach (T stackAllocatedValue in _stackAllocatedBuffer) + Debug.Assert(_array != null); // Nonzero _count should imply this + + T[] result = _array; + if (_count < result.Length) { - result[index++] = stackAllocatedValue; - if (index >= _count) - { - return result; - } + // Avoid a bit of overhead (method call, some branches, extra codegen) + // which would be incurred by using Array.Resize + result = new T[_count]; + Array.Copy(_array, result, _count); } - _array.AsSpan(0, _count - StackAllocatedCapacity).CopyTo(result.AsSpan(start: StackAllocatedCapacity)); - #if DEBUG // Try to prevent callers from using the ArrayBuilder after ToArray, if _count != 0. _count = -1; @@ -134,42 +132,25 @@ public T[] ToArray() public void UncheckedAdd(T item) { Debug.Assert(_count < Capacity); - if (_count < StackAllocatedCapacity) - { - _stackAllocatedBuffer[_count++] = item; - } - else - { - _array![_count++ - StackAllocatedCapacity] = item; - } + + _array![_count++] = item; } private void EnsureCapacity(int minimum) { Debug.Assert(minimum > Capacity); - if (minimum < StackAllocatedCapacity) - { - return; - } - - if (_array == null) - { - // Initial capacity has not been set correctly, we will use the default size - _array = new T[DefaultHeapCapacity]; - return; - } - - int nextHeapCapacity = 2 * _array.Length; + int capacity = Capacity; + int nextCapacity = capacity == 0 ? DefaultCapacity : 2 * capacity; - if ((uint)nextHeapCapacity > (uint)Array.MaxLength) + if ((uint)nextCapacity > (uint)Array.MaxLength) { - nextHeapCapacity = Math.Max(_array.Length + 1, Array.MaxLength); + nextCapacity = Math.Max(capacity + 1, Array.MaxLength); } - nextHeapCapacity = Math.Max(nextHeapCapacity, minimum); + nextCapacity = Math.Max(nextCapacity, minimum); - T[] next = new T[nextHeapCapacity]; + T[] next = new T[nextCapacity]; if (_count > 0) { Array.Copy(_array!, next, _count); From 9762cf410f1d9f24208fe1feba2fe45e98af975e Mon Sep 17 00:00:00 2001 From: Henrik Gedionsen Date: Tue, 7 Oct 2025 00:14:23 +0200 Subject: [PATCH 3/3] Proposal of stack based array builders, either fixed size and/or growing --- .../Collections/Generic/StackArrayBuilder.cs | 140 ++++++++++++++++++ .../Collections/Generic/StackArrayBuilder8.cs | 55 +++++++ .../src/System.Diagnostics.Process.csproj | 4 + 3 files changed, 199 insertions(+) create mode 100644 src/libraries/Common/src/System/Collections/Generic/StackArrayBuilder.cs create mode 100644 src/libraries/Common/src/System/Collections/Generic/StackArrayBuilder8.cs diff --git a/src/libraries/Common/src/System/Collections/Generic/StackArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/StackArrayBuilder.cs new file mode 100644 index 00000000000000..4c202b67641f8d --- /dev/null +++ b/src/libraries/Common/src/System/Collections/Generic/StackArrayBuilder.cs @@ -0,0 +1,140 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace StackArrayBuilder; + +/// +/// Helper type for avoiding allocations while building arrays. +/// +/// The element type. +/// +/// Will grow heap allocated size, if you need it. +/// Only use grow in rare cases, as it needs to grow the array, if over already allocated size. +/// If you are certain of the max size needed, you can use e.g. StackArrayBuilder8 +/// +internal ref struct StackArrayBuilder +{ + private InlineArray16 _stackAllocatedBuffer = default; + public const int StackAllocatedCapacity = 16; + private const int DefaultHeapCapacity = 4; + + private T[]? _heapArrayBuffer; // Starts out null, initialized if capacity is over stack allocated size when constructing or on Add. + private int _count; // Number of items added. + + /// + /// Initializes the with a specified capacity. + /// + /// The capacity of the array to allocate. + public StackArrayBuilder(int capacity) : this() + { + Debug.Assert(capacity >= 0); + if (capacity > StackAllocatedCapacity) + { + _heapArrayBuffer = new T[capacity - StackAllocatedCapacity]; + } + } + + /// + /// Gets the number of items this instance can store without re-allocating. + /// StackAllocatedCapacity if the backing heap array is not needed, all up to that is already stack allocated + /// + /// Only for unit testing, checking that overallocation does not happen + public int Capacity => _heapArrayBuffer?.Length + StackAllocatedCapacity ?? StackAllocatedCapacity; + + /// + /// Adds an item, resizing heap allocated array if necessary. + /// + /// The item to add. + public void Add(T item) + { + if (_count == Capacity) + { + EnsureCapacity(_count + 1); + } + + UncheckedAdd(item); + } + + /// + /// Creates an array from the contents of this builder. + /// + public T[] ToArray() + { + if (_count == 0) + { + return []; + } + + T[] result = new T[_count]; + int index = 0; + foreach (T stackAllocatedValue in _stackAllocatedBuffer) + { + result[index++] = stackAllocatedValue; + if (index >= _count) + { + return result; + } + } + + _heapArrayBuffer.AsSpan(0, _count - StackAllocatedCapacity).CopyTo(result.AsSpan(start: StackAllocatedCapacity)); + + return result; + } + + /// + /// Adds an item, without checking if there is room. + /// + /// The item to add. + /// + /// Use this method if you know there is enough space in the + /// for another item, and you are writing performance-sensitive code. + /// + public void UncheckedAdd(T item) + { + Debug.Assert(_count < Capacity); + if (_count < StackAllocatedCapacity) + { + _stackAllocatedBuffer[_count++] = item; + } + else + { + _heapArrayBuffer![_count++ - StackAllocatedCapacity] = item; + } + } + + private void EnsureCapacity(int minimum) + { + Debug.Assert(minimum > Capacity); + + if (minimum < StackAllocatedCapacity) + { + return; // There is still room on the stack + } + + if (_heapArrayBuffer == null) + { + // Initial capacity has not been not set or too low, we will allocate the default heap array size + _heapArrayBuffer = new T[DefaultHeapCapacity]; + return; + } + + // Check if allocated heap capacity was enough + int defaultCapacityWithHeap = _heapArrayBuffer.Length + StackAllocatedCapacity; + if (defaultCapacityWithHeap >= minimum) + { + return; // current allocated stack+heap is large enough + } + + // We need to allocate more heap capacity, by increasing the size of the array + int nextHeapCapacity = 2 * _heapArrayBuffer.Length; + + if ((uint)nextHeapCapacity > (uint)Array.MaxLength) + { + nextHeapCapacity = Math.Max(_heapArrayBuffer.Length + 1, Array.MaxLength); + } + + nextHeapCapacity = Math.Max(nextHeapCapacity, minimum); + + Array.Resize(ref _heapArrayBuffer, nextHeapCapacity); + } +} diff --git a/src/libraries/Common/src/System/Collections/Generic/StackArrayBuilder8.cs b/src/libraries/Common/src/System/Collections/Generic/StackArrayBuilder8.cs new file mode 100644 index 00000000000000..c17429e9f0f0b5 --- /dev/null +++ b/src/libraries/Common/src/System/Collections/Generic/StackArrayBuilder8.cs @@ -0,0 +1,55 @@ +using System.Runtime.CompilerServices; + +namespace StackArrayBuilder; + +/// +/// Helper type for avoiding allocations while building arrays. +/// +/// The element type. +/// +/// Throws InvalidOperationException, if the right size is not selected. +/// Be sure to select the right size to not over- or under-allocate expected size on stack. +/// +internal ref struct StackArrayBuilder8 +{ + private InlineArray8 _stackAllocatedBuffer = default; + public const int StackAllocatedCapacity = 8; + + private int _count = 0; // Number of items added. + + public StackArrayBuilder8() + { + } + + /// Adds an item + /// The item to add. + public void Add(T item) + { + if (_count == StackAllocatedCapacity) + { + throw new InvalidOperationException("Stack allocated capacity exceeded"); + } + _stackAllocatedBuffer[_count++] = item; + } + + /// Creates an array from the contents of this builder. + public T[] ToArray() + { + if (_count == 0) + { + return []; + } + + T[] result = new T[_count]; + int index = 0; + foreach (T stackAllocatedValue in _stackAllocatedBuffer) + { + result[index++] = stackAllocatedValue; + if (index >= _count) + { + return result; + } + } + return result; + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index ad692fafc90f2c..481c719722f9f0 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -47,6 +47,10 @@ Link="Common\System\Text\ValueStringBuilder.cs" /> + +