Skip to content

Commit cc92629

Browse files
committed
internal struct ValueStringBuilder; Optimizations of string concatenations (#109)
* internal struct ValueStringBuilder * stackalloc 256 chars
1 parent 5015c2e commit cc92629

9 files changed

Lines changed: 359 additions & 54 deletions

File tree

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: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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+
s.CopyTo(_chars.Slice(index));
160+
_pos += count;
161+
}
162+
163+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
164+
public void Append(char c)
165+
{
166+
int pos = _pos;
167+
Span<char> chars = _chars;
168+
if ((uint) pos < (uint) chars.Length) {
169+
chars[pos] = c;
170+
_pos = pos + 1;
171+
}
172+
else {
173+
GrowAndAppend(c);
174+
}
175+
}
176+
177+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
178+
public void Append(string? s)
179+
{
180+
if (s == null) {
181+
return;
182+
}
183+
184+
int pos = _pos;
185+
if (s.Length == 1 &&
186+
(uint) pos < (uint) _chars
187+
.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
188+
{
189+
_chars[pos] = s[0];
190+
_pos = pos + 1;
191+
}
192+
else {
193+
AppendSlow(s);
194+
}
195+
}
196+
197+
private void AppendSlow(string s)
198+
{
199+
int pos = _pos;
200+
if (pos > _chars.Length - s.Length) {
201+
Grow(s.Length);
202+
}
203+
204+
s.CopyTo(_chars.Slice(pos));
205+
_pos += s.Length;
206+
}
207+
208+
public void Append(char c, int count)
209+
{
210+
if (_pos > _chars.Length - count) {
211+
Grow(count);
212+
}
213+
214+
Span<char> dst = _chars.Slice(_pos, count);
215+
for (int i = 0; i < dst.Length; i++) {
216+
dst[i] = c;
217+
}
218+
219+
_pos += count;
220+
}
221+
222+
public void Append(ReadOnlySpan<char> value)
223+
{
224+
int pos = _pos;
225+
if (pos > _chars.Length - value.Length) {
226+
Grow(value.Length);
227+
}
228+
229+
value.CopyTo(_chars.Slice(_pos));
230+
_pos += value.Length;
231+
}
232+
233+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
234+
public Span<char> AppendSpan(int length)
235+
{
236+
int origPos = _pos;
237+
if (origPos > _chars.Length - length) {
238+
Grow(length);
239+
}
240+
241+
_pos = origPos + length;
242+
return _chars.Slice(origPos, length);
243+
}
244+
245+
[MethodImpl(MethodImplOptions.NoInlining)]
246+
private void GrowAndAppend(char c)
247+
{
248+
Grow(1);
249+
Append(c);
250+
}
251+
252+
/// <summary>
253+
/// Resize the internal buffer either by doubling current buffer size or
254+
/// by adding <paramref name="additionalCapacityBeyondPos"/> to
255+
/// <see cref="_pos"/> whichever is greater.
256+
/// </summary>
257+
/// <param name="additionalCapacityBeyondPos">
258+
/// Number of chars requested beyond current position.
259+
/// </param>
260+
[MethodImpl(MethodImplOptions.NoInlining)]
261+
private void Grow(int additionalCapacityBeyondPos)
262+
{
263+
Debug.Assert(additionalCapacityBeyondPos > 0);
264+
Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos,
265+
"Grow called incorrectly, no resize is needed.");
266+
267+
const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
268+
269+
// Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try
270+
// to double the size if possible, bounding the doubling to not go beyond the max array length.
271+
int newCapacity = (int) Math.Max(
272+
(uint) (_pos + additionalCapacityBeyondPos),
273+
Math.Min((uint) _chars.Length * 2, ArrayMaxLength));
274+
275+
// Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
276+
// This could also go negative if the actual required length wraps around.
277+
char[] poolArray = ArrayPool<char>.Shared.Rent(newCapacity);
278+
279+
_chars.Slice(0, _pos).CopyTo(poolArray);
280+
281+
char[]? toReturn = _arrayToReturnToPool;
282+
_chars = _arrayToReturnToPool = poolArray;
283+
if (toReturn != null) {
284+
ArrayPool<char>.Shared.Return(toReturn);
285+
}
286+
}
287+
288+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
289+
public void Dispose()
290+
{
291+
char[]? toReturn = _arrayToReturnToPool;
292+
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
293+
if (toReturn != null) {
294+
ArrayPool<char>.Shared.Return(toReturn);
295+
}
296+
}
297+
}
298+
}

0 commit comments

Comments
 (0)