StringExtensions.ToString(IEnumerable<Rune>) stackalloc char buffer with StringBuilder fallback

This commit is contained in:
Tonttu
2025-03-15 14:15:55 +02:00
committed by Tig
parent b6a5ca1d4e
commit 5ab51fc08b
2 changed files with 68 additions and 24 deletions

View File

@@ -16,27 +16,28 @@ public class ToStringEnumerable
/// </summary>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public string Previous (IEnumerable<Rune> runes, int size)
public string Previous (IEnumerable<Rune> runes, int len)
{
return StringAppendInLoop (runes);
return StringConcatInLoop (runes);
}
/// <summary>
/// Benchmark for current implementation with rune chars appending to StringBuilder.
/// Benchmark for current implementation with stackalloc char buffer and
/// fallback to rune chars appending to StringBuilder.
/// </summary>
/// <param name="runes"></param>
/// <returns></returns>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public string Current (IEnumerable<Rune> runes, int size)
public string Current (IEnumerable<Rune> runes, int len)
{
return Tui.StringExtensions.ToString (runes);
}
/// <summary>
/// Previous implementation with string append in a loop.
/// Previous implementation with string concatenation in a loop.
/// </summary>
private static string StringAppendInLoop (IEnumerable<Rune> runes)
private static string StringConcatInLoop (IEnumerable<Rune> runes)
{
var str = string.Empty;
@@ -50,21 +51,34 @@ public class ToStringEnumerable
public IEnumerable<object []> DataSource ()
{
string textSource =
"""
Ĺόŕéḿ íśúḿ d́όĺόŕ śí áḿé, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺí. ŕáéśéń q́úíś ĺúćt́úś éĺí. Íńt́éǵéŕ ú áŕćú éǵé d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć é d́íáḿ.
éĺĺéńt́éśq́úé śé d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Ú q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś í, t́éḿṕúś ńéq́úé.
ŕáéśéń śáíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś á, v́áŕíúś śúśćíí áńt́é. Ú úĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
Óŕćí v́áŕíúś ńát́όq́úé éńát́íb́úś é ḿáǵńíś d́íś áŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé á é b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ é, v́úĺṕút́át́é ĺáćúś.
Śúśṕéńd́íśśé śí áḿé áŕćú ú áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śí áḿé ḿáx́íḿúś d́íáḿ. Ńáḿ é ĺéό, h́áŕét́ŕá éú ĺόb́όŕt́íś á, t́ŕíśt́íq́úé ú f́éĺíś.
""";
// Extra argument as workaround for the summary grouping different length collections to same baseline making comparison difficult.
int[] sizes = [1, 10, 100, textSource.Length / 2, textSource.Length];
foreach (int size in sizes)
// Extra length argument as workaround for the summary grouping
// different length collections to same baseline making comparison difficult.
foreach (string text in GetTextData ())
{
yield return [textSource.EnumerateRunes ().Take (size).ToArray (), size];
Rune [] runes = [..text.EnumerateRunes ()];
yield return [runes, runes.Length];
}
}
private IEnumerable<string> GetTextData ()
{
string textSource =
"""
Ĺόŕéḿ íśúḿ d́όĺόŕ śí áḿé, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺí. ŕáéśéń q́úíś ĺúćt́úś éĺí. Íńt́éǵéŕ ú áŕćú éǵé d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć é d́íáḿ.
éĺĺéńt́éśq́úé śé d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Ú q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś í, t́éḿṕúś ńéq́úé.
ŕáéśéń śáíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś á, v́áŕíúś śúśćíí áńt́é. Ú úĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
Óŕćí v́áŕíúś ńát́όq́úé éńát́íb́úś é ḿáǵńíś d́íś áŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé á é b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ é, v́úĺṕút́át́é ĺáćúś.
Śúśṕéńd́íśśé śí áḿé áŕćú ú áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śí áḿé ḿáx́íḿúś d́íáḿ. Ńáḿ é ĺéό, h́áŕét́ŕá éú ĺόb́όŕt́íś á, t́ŕíśt́íq́úé ú f́éĺíś.
""";
int[] lengths = [1, 10, 100, textSource.Length / 2, textSource.Length];
foreach (int length in lengths)
{
yield return textSource [..length];
}
string textLongerThanStackallocThreshold = string.Concat(Enumerable.Repeat(textSource, 10));
yield return textLongerThanStackallocThreshold;
}
}

View File

@@ -124,13 +124,43 @@ public static class StringExtensions
/// <returns></returns>
public static string ToString (IEnumerable<Rune> runes)
{
StringBuilder stringBuilder = new();
const int maxCharsPerRune = 2;
Span<char> charBuffer = stackalloc char[maxCharsPerRune];
// Max stackalloc ~2 kB
const int maxStackallocTextBufferSize = 1048;
Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
// Use stackalloc buffer if rune count is easily available and the count is reasonable.
if (runes.TryGetNonEnumeratedCount (out int count))
{
if (count == 0)
{
return string.Empty;
}
int maxRequiredTextBufferSize = count * maxCharsPerRune;
if (maxRequiredTextBufferSize <= maxStackallocTextBufferSize)
{
Span<char> textBuffer = stackalloc char[maxRequiredTextBufferSize];
Span<char> remainingBuffer = textBuffer;
foreach (Rune rune in runes)
{
int charsWritten = rune.EncodeToUtf16 (runeBuffer);
ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
runeChars.CopyTo (remainingBuffer);
remainingBuffer = remainingBuffer [runeChars.Length..];
}
ReadOnlySpan<char> text = textBuffer[..^remainingBuffer.Length];
return text.ToString ();
}
}
// Fallback to StringBuilder append.
StringBuilder stringBuilder = new();
foreach (Rune rune in runes)
{
int charsWritten = rune.EncodeToUtf16 (charBuffer);
ReadOnlySpan<char> runeChars = charBuffer [..charsWritten];
int charsWritten = rune.EncodeToUtf16 (runeBuffer);
ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
stringBuilder.Append (runeChars);
}
return stringBuilder.ToString ();