Skip to content

Commit 5ab51fc

Browse files
TheTonttutig
authored andcommitted
StringExtensions.ToString(IEnumerable<Rune>) stackalloc char buffer with StringBuilder fallback
1 parent b6a5ca1 commit 5ab51fc

File tree

2 files changed

+64
-20
lines changed

2 files changed

+64
-20
lines changed

Benchmarks/Text/StringExtensions/ToStringEnumerable.cs

+30-16
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,28 @@ public class ToStringEnumerable
1616
/// </summary>
1717
[Benchmark]
1818
[ArgumentsSource (nameof (DataSource))]
19-
public string Previous (IEnumerable<Rune> runes, int size)
19+
public string Previous (IEnumerable<Rune> runes, int len)
2020
{
21-
return StringAppendInLoop (runes);
21+
return StringConcatInLoop (runes);
2222
}
2323

2424
/// <summary>
25-
/// Benchmark for current implementation with rune chars appending to StringBuilder.
25+
/// Benchmark for current implementation with stackalloc char buffer and
26+
/// fallback to rune chars appending to StringBuilder.
2627
/// </summary>
2728
/// <param name="runes"></param>
2829
/// <returns></returns>
2930
[Benchmark (Baseline = true)]
3031
[ArgumentsSource (nameof (DataSource))]
31-
public string Current (IEnumerable<Rune> runes, int size)
32+
public string Current (IEnumerable<Rune> runes, int len)
3233
{
3334
return Tui.StringExtensions.ToString (runes);
3435
}
3536

3637
/// <summary>
37-
/// Previous implementation with string append in a loop.
38+
/// Previous implementation with string concatenation in a loop.
3839
/// </summary>
39-
private static string StringAppendInLoop (IEnumerable<Rune> runes)
40+
private static string StringConcatInLoop (IEnumerable<Rune> runes)
4041
{
4142
var str = string.Empty;
4243

@@ -49,22 +50,35 @@ private static string StringAppendInLoop (IEnumerable<Rune> runes)
4950
}
5051

5152
public IEnumerable<object []> DataSource ()
53+
{
54+
// Extra length argument as workaround for the summary grouping
55+
// different length collections to same baseline making comparison difficult.
56+
foreach (string text in GetTextData ())
57+
{
58+
Rune [] runes = [..text.EnumerateRunes ()];
59+
yield return [runes, runes.Length];
60+
}
61+
}
62+
63+
private IEnumerable<string> GetTextData ()
5264
{
5365
string textSource =
5466
"""
55-
Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
56-
Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
57-
Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
58-
Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
59-
Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
60-
""";
67+
Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
68+
Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
69+
Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
70+
Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
71+
Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
72+
""";
6173

62-
// Extra argument as workaround for the summary grouping different length collections to same baseline making comparison difficult.
63-
int[] sizes = [1, 10, 100, textSource.Length / 2, textSource.Length];
74+
int[] lengths = [1, 10, 100, textSource.Length / 2, textSource.Length];
6475

65-
foreach (int size in sizes)
76+
foreach (int length in lengths)
6677
{
67-
yield return [textSource.EnumerateRunes ().Take (size).ToArray (), size];
78+
yield return textSource [..length];
6879
}
80+
81+
string textLongerThanStackallocThreshold = string.Concat(Enumerable.Repeat(textSource, 10));
82+
yield return textLongerThanStackallocThreshold;
6983
}
7084
}

Terminal.Gui/Text/StringExtensions.cs

+34-4
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,43 @@ public static (Rune Rune, int Size) DecodeRune (this string str, int start = 0,
124124
/// <returns></returns>
125125
public static string ToString (IEnumerable<Rune> runes)
126126
{
127-
StringBuilder stringBuilder = new();
128127
const int maxCharsPerRune = 2;
129-
Span<char> charBuffer = stackalloc char[maxCharsPerRune];
128+
// Max stackalloc ~2 kB
129+
const int maxStackallocTextBufferSize = 1048;
130+
131+
Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
132+
// Use stackalloc buffer if rune count is easily available and the count is reasonable.
133+
if (runes.TryGetNonEnumeratedCount (out int count))
134+
{
135+
if (count == 0)
136+
{
137+
return string.Empty;
138+
}
139+
140+
int maxRequiredTextBufferSize = count * maxCharsPerRune;
141+
if (maxRequiredTextBufferSize <= maxStackallocTextBufferSize)
142+
{
143+
Span<char> textBuffer = stackalloc char[maxRequiredTextBufferSize];
144+
Span<char> remainingBuffer = textBuffer;
145+
foreach (Rune rune in runes)
146+
{
147+
int charsWritten = rune.EncodeToUtf16 (runeBuffer);
148+
ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
149+
runeChars.CopyTo (remainingBuffer);
150+
remainingBuffer = remainingBuffer [runeChars.Length..];
151+
}
152+
153+
ReadOnlySpan<char> text = textBuffer[..^remainingBuffer.Length];
154+
return text.ToString ();
155+
}
156+
}
157+
158+
// Fallback to StringBuilder append.
159+
StringBuilder stringBuilder = new();
130160
foreach (Rune rune in runes)
131161
{
132-
int charsWritten = rune.EncodeToUtf16 (charBuffer);
133-
ReadOnlySpan<char> runeChars = charBuffer [..charsWritten];
162+
int charsWritten = rune.EncodeToUtf16 (runeBuffer);
163+
ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
134164
stringBuilder.Append (runeChars);
135165
}
136166
return stringBuilder.ToString ();

0 commit comments

Comments
 (0)