Skip to content

Commit 471dd5c

Browse files
authored
Add a ValueStringBuilder for .NET 6 (#7962)
## Summary of changes Adds a `ValueStringBuilder` implementation, based on the one used internally in the .NET runtime ## Reason for change - We can stackalloc the `Span<char>` - It's a bit faster than our existing `StringBuilderCache` implementation, and in some places that matters. ## Implementation details - Made in .NET 6 only for simplicity. We _could_ expose it earlier, but I wanted this for the updated aspnetcore integration, so .NET6+ only for now is good enough - It's not without risks in its usage, so we have to be careful about things like passing it around (i.e. avoid doing that completely, for safety) - Uses an array pool backed implementation (again, built into .NET 6) ## Test coverage Imported the unit tests from the runtime too ## Other details https://datadoghq.atlassian.net/browse/LANGPLAT-842 Part of a stack - #7962 👈 - #7963 - #7964 - #7966 - #7965
1 parent 2039ce6 commit 471dd5c

File tree

3 files changed

+605
-0
lines changed

3 files changed

+605
-0
lines changed

tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,7 @@
672672
<type fullname="System.IO.UnmanagedMemoryStream" />
673673
<type fullname="System.IObservable`1" />
674674
<type fullname="System.IObserver`1" />
675+
<type fullname="System.ISpanFormattable" />
675676
<type fullname="System.Lazy`1" />
676677
<type fullname="System.Math" />
677678
<type fullname="System.MethodAccessException" />
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
// <copyright file="ValueStringBuilder.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if NET6_0_OR_GREATER
7+
// Licensed to the .NET Foundation under one or more agreements.
8+
// The .NET Foundation licenses this file to you under the MIT license.
9+
10+
using System;
11+
using System.Buffers;
12+
using System.Diagnostics;
13+
using System.Runtime.CompilerServices;
14+
using System.Runtime.InteropServices;
15+
16+
#nullable enable
17+
18+
namespace Datadog.Trace.Util
19+
{
20+
internal ref struct ValueStringBuilder
21+
{
22+
private char[]? _arrayToReturnToPool;
23+
private Span<char> _chars;
24+
private int _pos;
25+
26+
public ValueStringBuilder(Span<char> initialBuffer)
27+
{
28+
_arrayToReturnToPool = null;
29+
_chars = initialBuffer;
30+
_pos = 0;
31+
}
32+
33+
public ValueStringBuilder(int initialCapacity)
34+
{
35+
_arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
36+
_chars = _arrayToReturnToPool;
37+
_pos = 0;
38+
}
39+
40+
/// <summary>Gets the underlying storage of the builder.</summary>
41+
public Span<char> RawChars => _chars;
42+
43+
public int Length
44+
{
45+
get => _pos;
46+
set
47+
{
48+
_pos = value;
49+
}
50+
}
51+
52+
public int Capacity => _chars.Length;
53+
54+
public ref char this[int index]
55+
{
56+
get
57+
{
58+
return ref _chars[index];
59+
}
60+
}
61+
62+
public void EnsureCapacity(int capacity)
63+
{
64+
// This is not expected to be called this with negative capacity
65+
66+
// If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception.
67+
if ((uint)capacity > (uint)_chars.Length)
68+
{
69+
Grow(capacity - _pos);
70+
}
71+
}
72+
73+
/// <summary>
74+
/// Ensures that the builder is terminated with a NUL character.
75+
/// </summary>
76+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
77+
public void NullTerminate()
78+
{
79+
EnsureCapacity(_pos + 1);
80+
_chars[_pos] = '\0';
81+
}
82+
83+
/// <summary>
84+
/// Get a pinnable reference to the builder.
85+
/// Does not ensure there is a null char after <see cref="Length"/>
86+
/// This overload is pattern matched in the C# 7.3+ compiler so you can omit
87+
/// the explicit method call, and write eg "fixed (char* c = builder)"
88+
/// </summary>
89+
public ref char GetPinnableReference()
90+
{
91+
return ref MemoryMarshal.GetReference(_chars);
92+
}
93+
94+
public override string ToString()
95+
{
96+
string s = _chars.Slice(0, _pos).ToString();
97+
Dispose();
98+
return s;
99+
}
100+
101+
public ReadOnlySpan<char> AsSpan() => _chars.Slice(0, _pos);
102+
103+
public ReadOnlySpan<char> AsSpan(int start) => _chars.Slice(start, _pos - start);
104+
105+
public ReadOnlySpan<char> AsSpan(int start, int length) => _chars.Slice(start, length);
106+
107+
public void Insert(int index, char value, int count)
108+
{
109+
if (_pos > _chars.Length - count)
110+
{
111+
Grow(count);
112+
}
113+
114+
int remaining = _pos - index;
115+
_chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
116+
_chars.Slice(index, count).Fill(value);
117+
_pos += count;
118+
}
119+
120+
public void Insert(int index, string? s)
121+
{
122+
if (s == null)
123+
{
124+
return;
125+
}
126+
127+
int count = s.Length;
128+
129+
if (_pos > (_chars.Length - count))
130+
{
131+
Grow(count);
132+
}
133+
134+
int remaining = _pos - index;
135+
_chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
136+
s
137+
#if !NET
138+
.AsSpan()
139+
#endif
140+
.CopyTo(_chars.Slice(index));
141+
_pos += count;
142+
}
143+
144+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
145+
public void Append(char c)
146+
{
147+
int pos = _pos;
148+
Span<char> chars = _chars;
149+
if ((uint)pos < (uint)chars.Length)
150+
{
151+
chars[pos] = c;
152+
_pos = pos + 1;
153+
}
154+
else
155+
{
156+
GrowAndAppend(c);
157+
}
158+
}
159+
160+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
161+
public void Append(string? s)
162+
{
163+
if (s == null)
164+
{
165+
return;
166+
}
167+
168+
int pos = _pos;
169+
// very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
170+
if (s.Length == 1 && (uint)pos < (uint)_chars.Length)
171+
{
172+
_chars[pos] = s[0];
173+
_pos = pos + 1;
174+
}
175+
else
176+
{
177+
AppendSlow(s);
178+
}
179+
}
180+
181+
private void AppendSlow(string s)
182+
{
183+
int pos = _pos;
184+
if (pos > _chars.Length - s.Length)
185+
{
186+
Grow(s.Length);
187+
}
188+
189+
s
190+
#if !NET
191+
.AsSpan()
192+
#endif
193+
.CopyTo(_chars.Slice(pos));
194+
_pos += s.Length;
195+
}
196+
197+
public void Append(char c, int count)
198+
{
199+
if (_pos > _chars.Length - count)
200+
{
201+
Grow(count);
202+
}
203+
204+
Span<char> dst = _chars.Slice(_pos, count);
205+
for (int i = 0; i < dst.Length; i++)
206+
{
207+
dst[i] = c;
208+
}
209+
210+
_pos += count;
211+
}
212+
213+
public void Append(scoped ReadOnlySpan<char> value)
214+
{
215+
int pos = _pos;
216+
if (pos > _chars.Length - value.Length)
217+
{
218+
Grow(value.Length);
219+
}
220+
221+
value.CopyTo(_chars.Slice(_pos));
222+
_pos += value.Length;
223+
}
224+
225+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
226+
public Span<char> AppendSpan(int length)
227+
{
228+
int origPos = _pos;
229+
if (origPos > _chars.Length - length)
230+
{
231+
Grow(length);
232+
}
233+
234+
_pos = origPos + length;
235+
return _chars.Slice(origPos, length);
236+
}
237+
238+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
239+
public void AppendAsLowerInvariant(scoped ReadOnlySpan<char> value)
240+
{
241+
int pos = _pos;
242+
if (pos > _chars.Length - value.Length)
243+
{
244+
Grow(value.Length);
245+
}
246+
247+
value.ToLowerInvariant(_chars.Slice(_pos));
248+
_pos += value.Length;
249+
}
250+
251+
[MethodImpl(MethodImplOptions.NoInlining)]
252+
private void GrowAndAppend(char c)
253+
{
254+
Grow(1);
255+
Append(c);
256+
}
257+
258+
/// <summary>
259+
/// Resize the internal buffer either by doubling current buffer size or
260+
/// by adding <paramref name="additionalCapacityBeyondPos"/> to
261+
/// <see cref="_pos"/> whichever is greater.
262+
/// </summary>
263+
/// <param name="additionalCapacityBeyondPos">
264+
/// Number of chars requested beyond current position.
265+
/// </param>
266+
[MethodImpl(MethodImplOptions.NoInlining)]
267+
private void Grow(int additionalCapacityBeyondPos)
268+
{
269+
const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
270+
271+
// Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try
272+
// to double the size if possible, bounding the doubling to not go beyond the max array length.
273+
int newCapacity = (int)Math.Max(
274+
(uint)(_pos + additionalCapacityBeyondPos),
275+
Math.Min((uint)_chars.Length * 2, ArrayMaxLength));
276+
277+
// Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
278+
// This could also go negative if the actual required length wraps around.
279+
char[] poolArray = ArrayPool<char>.Shared.Rent(newCapacity);
280+
281+
_chars.Slice(0, _pos).CopyTo(poolArray);
282+
283+
char[]? toReturn = _arrayToReturnToPool;
284+
_chars = _arrayToReturnToPool = poolArray;
285+
if (toReturn != null)
286+
{
287+
ArrayPool<char>.Shared.Return(toReturn);
288+
}
289+
}
290+
291+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
292+
public void Dispose()
293+
{
294+
char[]? toReturn = _arrayToReturnToPool;
295+
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
296+
if (toReturn != null)
297+
{
298+
ArrayPool<char>.Shared.Return(toReturn);
299+
}
300+
}
301+
302+
internal void AppendSpanFormattable<T>(T value, string? format = null, IFormatProvider? provider = null)
303+
where T : ISpanFormattable
304+
{
305+
if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider))
306+
{
307+
_pos += charsWritten;
308+
}
309+
else
310+
{
311+
Append(value.ToString(format, provider));
312+
}
313+
}
314+
}
315+
}
316+
#endif

0 commit comments

Comments
 (0)