Skip to content

Commit 2bca4c3

Browse files
perf: use ValueStringBuilder to avoid allocations
1 parent 12a0fa9 commit 2bca4c3

File tree

3 files changed

+604
-131
lines changed

3 files changed

+604
-131
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
using System.Buffers;
2+
using System.Diagnostics;
3+
using System.Runtime.CompilerServices;
4+
5+
namespace TUnit.Engine.Helpers;
6+
7+
// From https://github.com/dotnet/runtime/blob/d968dc4bbdc0c26876c2cdaadf42740e891586b9/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs#L8
8+
internal ref partial struct ValueListBuilder<T>
9+
{
10+
private Span<T> _span;
11+
private T[]? _arrayFromPool;
12+
private int _pos;
13+
14+
public ValueListBuilder(Span<T?> scratchBuffer)
15+
{
16+
_span = scratchBuffer!;
17+
}
18+
19+
public ValueListBuilder(int capacity)
20+
{
21+
Grow(capacity);
22+
}
23+
24+
public int Length
25+
{
26+
get => _pos;
27+
set
28+
{
29+
Debug.Assert(value >= 0);
30+
Debug.Assert(value <= _span.Length);
31+
_pos = value;
32+
}
33+
}
34+
35+
public ref T this[int index]
36+
{
37+
get
38+
{
39+
Debug.Assert(index < _pos);
40+
return ref _span[index];
41+
}
42+
}
43+
44+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
45+
public void Append(T item)
46+
{
47+
int pos = _pos;
48+
49+
// Workaround for https://github.com/dotnet/runtime/issues/72004
50+
Span<T> span = _span;
51+
if ((uint)pos < (uint)span.Length)
52+
{
53+
span[pos] = item;
54+
_pos = pos + 1;
55+
}
56+
else
57+
{
58+
AddWithResize(item);
59+
}
60+
}
61+
62+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
63+
public void Append(scoped ReadOnlySpan<T> source)
64+
{
65+
int pos = _pos;
66+
Span<T> span = _span;
67+
if (source.Length == 1 && (uint)pos < (uint)span.Length)
68+
{
69+
span[pos] = source[0];
70+
_pos = pos + 1;
71+
}
72+
else
73+
{
74+
AppendMultiChar(source);
75+
}
76+
}
77+
78+
[MethodImpl(MethodImplOptions.NoInlining)]
79+
private void AppendMultiChar(scoped ReadOnlySpan<T> source)
80+
{
81+
if ((uint)(_pos + source.Length) > (uint)_span.Length)
82+
{
83+
Grow(_span.Length - _pos + source.Length);
84+
}
85+
86+
source.CopyTo(_span.Slice(_pos));
87+
_pos += source.Length;
88+
}
89+
90+
public void Insert(int index, scoped ReadOnlySpan<T> source)
91+
{
92+
Debug.Assert(index == 0, "Implementation currently only supports index == 0");
93+
94+
if ((uint)(_pos + source.Length) > (uint)_span.Length)
95+
{
96+
Grow(source.Length);
97+
}
98+
99+
_span.Slice(0, _pos).CopyTo(_span.Slice(source.Length));
100+
source.CopyTo(_span);
101+
_pos += source.Length;
102+
}
103+
104+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105+
public Span<T> AppendSpan(int length)
106+
{
107+
Debug.Assert(length >= 0);
108+
109+
int pos = _pos;
110+
Span<T> span = _span;
111+
if ((ulong)(uint)pos + (ulong)(uint)length <= (ulong)(uint)span.Length) // same guard condition as in Span<T>.Slice on 64-bit
112+
{
113+
_pos = pos + length;
114+
return span.Slice(pos, length);
115+
}
116+
else
117+
{
118+
return AppendSpanWithGrow(length);
119+
}
120+
}
121+
122+
[MethodImpl(MethodImplOptions.NoInlining)]
123+
private Span<T> AppendSpanWithGrow(int length)
124+
{
125+
int pos = _pos;
126+
Grow(_span.Length - pos + length);
127+
_pos += length;
128+
return _span.Slice(pos, length);
129+
}
130+
131+
// Hide uncommon path
132+
[MethodImpl(MethodImplOptions.NoInlining)]
133+
private void AddWithResize(T item)
134+
{
135+
Debug.Assert(_pos == _span.Length);
136+
int pos = _pos;
137+
Grow(1);
138+
_span[pos] = item;
139+
_pos = pos + 1;
140+
}
141+
142+
public ReadOnlySpan<T> AsSpan()
143+
{
144+
return _span.Slice(0, _pos);
145+
}
146+
147+
public bool TryCopyTo(Span<T> destination, out int itemsWritten)
148+
{
149+
if (_span.Slice(0, _pos).TryCopyTo(destination))
150+
{
151+
itemsWritten = _pos;
152+
return true;
153+
}
154+
155+
itemsWritten = 0;
156+
return false;
157+
}
158+
159+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
160+
public void Dispose()
161+
{
162+
T[]? toReturn = _arrayFromPool;
163+
if (toReturn != null)
164+
{
165+
_arrayFromPool = null;
166+
167+
#if SYSTEM_PRIVATE_CORELIB
168+
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
169+
{
170+
ArrayPool<T>.Shared.Return(toReturn, _pos);
171+
}
172+
else
173+
{
174+
ArrayPool<T>.Shared.Return(toReturn);
175+
}
176+
#else
177+
if (!typeof(T).IsPrimitive)
178+
{
179+
Array.Clear(toReturn, 0, _pos);
180+
}
181+
182+
ArrayPool<T>.Shared.Return(toReturn);
183+
#endif
184+
}
185+
}
186+
187+
// Note that consuming implementations depend on the list only growing if it's absolutely
188+
// required. If the list is already large enough to hold the additional items be added,
189+
// it must not grow. The list is used in a number of places where the reference is checked
190+
// and it's expected to match the initial reference provided to the constructor if that
191+
// span was sufficiently large.
192+
private void Grow(int additionalCapacityRequired = 1)
193+
{
194+
const int ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
195+
196+
// Double the size of the span. If it's currently empty, default to size 4,
197+
// although it'll be increased in Rent to the pool's minimum bucket size.
198+
int nextCapacity = Math.Max(_span.Length != 0 ? _span.Length * 2 : 4, _span.Length + additionalCapacityRequired);
199+
200+
// If the computed doubled capacity exceeds the possible length of an array, then we
201+
// want to downgrade to either the maximum array length if that's large enough to hold
202+
// an additional item, or the current length + 1 if it's larger than the max length, in
203+
// which case it'll result in an OOM when calling Rent below. In the exceedingly rare
204+
// case where _span.Length is already int.MaxValue (in which case it couldn't be a managed
205+
// array), just use that same value again and let it OOM in Rent as well.
206+
if ((uint)nextCapacity > ArrayMaxLength)
207+
{
208+
nextCapacity = Math.Max(Math.Max(_span.Length + 1, ArrayMaxLength), _span.Length);
209+
}
210+
211+
T[] array = ArrayPool<T>.Shared.Rent(nextCapacity);
212+
_span.CopyTo(array);
213+
214+
T[]? toReturn = _arrayFromPool;
215+
_span = _arrayFromPool = array;
216+
if (toReturn != null)
217+
{
218+
#if SYSTEM_PRIVATE_CORELIB
219+
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
220+
{
221+
ArrayPool<T>.Shared.Return(toReturn, _pos);
222+
}
223+
else
224+
{
225+
ArrayPool<T>.Shared.Return(toReturn);
226+
}
227+
#else
228+
if (!typeof(T).IsPrimitive)
229+
{
230+
Array.Clear(toReturn, 0, _pos);
231+
}
232+
233+
ArrayPool<T>.Shared.Return(toReturn);
234+
#endif
235+
}
236+
}
237+
}

0 commit comments

Comments
 (0)