Skip to content

Commit 4309951

Browse files
authored
Merge pull request #322 from servicetitan/upstream/ValueStringBuilder
`internal struct ValueStringBuilder`; Optimizations of string concatenations
2 parents 3091198 + de6a317 commit 4309951

File tree

9 files changed

+367
-54
lines changed

9 files changed

+367
-54
lines changed

Orm/Xtensive.Orm/Core/Extensions/EnumerableExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ public static string ToDelimitedString<TItem>(this IEnumerable<TItem> source, st
200200
{
201201
if (source==null)
202202
return string.Empty;
203-
var sb = new StringBuilder();
203+
var sb = new ValueStringBuilder(stackalloc char[4096]);
204204
bool insertDelimiter = false;
205205
foreach (var item in source) {
206206
if (insertDelimiter)
207207
sb.Append(delimiter);
208-
sb.Append(item);
208+
sb.Append(item?.ToString());
209209
insertDelimiter = true;
210210
}
211211
return sb.ToString();
@@ -222,12 +222,12 @@ public static string ToDelimitedString(this IEnumerable source, string separator
222222
{
223223
if (source==null)
224224
return string.Empty;
225-
var sb = new StringBuilder();
225+
var sb = new ValueStringBuilder(stackalloc char[4096]);
226226
bool insertDelimiter = false;
227227
foreach (object item in source) {
228228
if (insertDelimiter)
229229
sb.Append(separator);
230-
sb.Append(item);
230+
sb.Append(item?.ToString());
231231
insertDelimiter = true;
232232
}
233233
return sb.ToString();

Orm/Xtensive.Orm/Core/Extensions/StringExtensions.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public static string Indent(this string value, int indentSize, bool indentFirstL
9696
{
9797
ArgumentValidator.EnsureArgumentNotNull(value, "value");
9898
var indent = new string(' ', indentSize);
99-
var sb = new StringBuilder();
99+
var sb = new ValueStringBuilder(stackalloc char[4096]);
100100
if (indentFirstLine)
101101
sb.Append(indent);
102102
int start = 0;
@@ -207,7 +207,7 @@ public static string RevertibleJoin(this IEnumerable<string> source, char escape
207207
throw new ArgumentException(
208208
Strings.ExEscapeCharacterMustDifferFromDelimiterCharacter);
209209

210-
var sb = new StringBuilder();
210+
var sb = new ValueStringBuilder(stackalloc char[4096]);
211211
bool needDelimiter = false;
212212
foreach (var part in source) {
213213
if (needDelimiter)
@@ -283,7 +283,7 @@ public static Pair<string> RevertibleSplitFirstAndTail(this string source, char
283283
throw new ArgumentException(
284284
Strings.ExEscapeCharacterMustDifferFromDelimiterCharacter);
285285

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

0 commit comments

Comments
 (0)