From a4e68c7cb052b9f817281016d00717f5cf7da944 Mon Sep 17 00:00:00 2001 From: Garume <80187947+Garume@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:04:48 +0900 Subject: [PATCH 1/2] Avoid renting buffers for short encoded StringPool inputs --- .../Buffers/StringPool.cs | 25 +++ .../Buffers/Test_StringPool.cs | 156 ++++++++++++++++++ 2 files changed, 181 insertions(+) diff --git a/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs b/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs index abe3aeb27..9f6d2fd5d 100644 --- a/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs +++ b/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs @@ -223,6 +223,11 @@ public unsafe string GetOrAdd(ReadOnlySpan span, Encoding encoding) int maxLength = encoding.GetMaxCharCount(span.Length); + if ((uint)maxLength <= 64) + { + return GetOrAddWithStackBuffer(span, encoding, maxLength); + } + using SpanOwner buffer = SpanOwner.Allocate(maxLength); fixed (byte* source = span) @@ -234,6 +239,26 @@ public unsafe string GetOrAdd(ReadOnlySpan span, Encoding encoding) } } + /// + /// Decodes a bounded byte span using a stack-allocated buffer and returns its pooled string. + /// + /// The byte span to decode. + /// The encoding to use. + /// The maximum decoded character count. + /// The pooled string for the decoded characters. + private unsafe string GetOrAddWithStackBuffer(ReadOnlySpan span, Encoding encoding, int maxLength) + { + Span stackBuffer = stackalloc char[64]; + + fixed (byte* source = span) + fixed (char* destination = stackBuffer) + { + int effectiveLength = encoding.GetChars(source, span.Length, destination, maxLength); + + return GetOrAdd(new ReadOnlySpan(destination, effectiveLength)); + } + } + /// /// Tries to get a cached instance matching the input content, if present. /// diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Buffers/Test_StringPool.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Buffers/Test_StringPool.cs index c9140c49d..d68438a07 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/Buffers/Test_StringPool.cs +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Buffers/Test_StringPool.cs @@ -326,4 +326,160 @@ static bool IsMinHeap(uint[] array) Assert.IsTrue(IsMinHeap(array)); } } + + [TestMethod] + [DataRow(62, 63)] + [DataRow(63, 64)] + [DataRow(64, 65)] + [DataRow(126, 127)] + [DataRow(127, 128)] + [DataRow(128, 129)] + public void Test_StringPool_GetOrAdd_Encoding_Utf8_BufferBoundary(int inputLength, int expectedMaxLength) + { + StringPool pool = new(); + string text = new('a', inputLength); + byte[] bytes = Encoding.UTF8.GetBytes(text); + + Assert.AreEqual(expectedMaxLength, Encoding.UTF8.GetMaxCharCount(bytes.Length)); + + string first = pool.GetOrAdd(bytes, Encoding.UTF8); + string second = pool.GetOrAdd(bytes, Encoding.UTF8); + + Assert.AreEqual(text, first); + Assert.AreSame(first, second); + } + + [TestMethod] + public void Test_StringPool_GetOrAdd_Encoding_Unicode_SmallAndLarge() + { + string small = "Zażółć 😀 東京"; + string large = string.Concat(Enumerable.Repeat("Zażółć 😀 東京 ", 32)); + + foreach (Encoding encoding in new[] { Encoding.UTF8, Encoding.Unicode }) + { + byte[] smallBytes = encoding.GetBytes(small); + byte[] largeBytes = encoding.GetBytes(large); + + Assert.IsLessThanOrEqualTo(64, encoding.GetMaxCharCount(smallBytes.Length)); + Assert.IsGreaterThan(64, encoding.GetMaxCharCount(largeBytes.Length)); + + StringPool pool = new(); + + string smallFirst = pool.GetOrAdd(smallBytes, encoding); + string smallSecond = pool.GetOrAdd(smallBytes, encoding); + string largeFirst = pool.GetOrAdd(largeBytes, encoding); + string largeSecond = pool.GetOrAdd(largeBytes, encoding); + + Assert.AreEqual(small, smallFirst); + Assert.AreSame(smallFirst, smallSecond); + Assert.AreEqual(large, largeFirst); + Assert.AreSame(largeFirst, largeSecond); + } + } + + [TestMethod] + [DataRow(1)] + [DataRow(128)] + public void Test_StringPool_GetOrAdd_Encoding_InvalidUtf8(int inputLength) + { + byte[] bytes = Enumerable.Repeat((byte)0xFF, inputLength).ToArray(); + Encoding replacementEncoding = new UTF8Encoding(false, false); + Encoding throwingEncoding = new UTF8Encoding(false, true); + StringPool pool = new(); + + if (inputLength == 1) + { + Assert.IsLessThanOrEqualTo(64, replacementEncoding.GetMaxCharCount(bytes.Length)); + } + else + { + Assert.IsGreaterThan(64, replacementEncoding.GetMaxCharCount(bytes.Length)); + } + + Assert.AreEqual(replacementEncoding.GetString(bytes), pool.GetOrAdd(bytes, replacementEncoding)); + _ = Assert.ThrowsExactly(() => pool.GetOrAdd(bytes, throwingEncoding)); + } + + [TestMethod] + public void Test_StringPool_GetOrAdd_Encoding_Empty_AllowsNullEncoding() + { + StringPool pool = new(); + + Assert.AreSame(string.Empty, pool.GetOrAdd(ReadOnlySpan.Empty, null!)); + _ = Assert.ThrowsExactly(() => pool.GetOrAdd(new byte[] { 1 }, null!)); + } + + [TestMethod] + [DataRow(0, "")] + [DataRow(129, "A")] + public void Test_StringPool_GetOrAdd_Encoding_PassesExactCapacityAndNonNullDestination(int maxCharCount, string expected) + { + TrackingEncoding encoding = new(maxCharCount); + StringPool pool = new(); + + string result = pool.GetOrAdd(new byte[] { (byte)'A' }, encoding); + + Assert.AreEqual(expected, result); + Assert.AreEqual(maxCharCount, encoding.LastCharCapacity); + Assert.IsTrue(encoding.DestinationWasNonNull); + } + + private unsafe sealed class TrackingEncoding : Encoding + { + private readonly int maxCharCount; + + public TrackingEncoding(int maxCharCount) + { + this.maxCharCount = maxCharCount; + } + + public int LastCharCapacity { get; private set; } + + public bool DestinationWasNonNull { get; private set; } + + public override int GetMaxCharCount(int byteCount) + { + return this.maxCharCount; + } + + public override int GetMaxByteCount(int charCount) + { + return charCount; + } + + public override int GetChars(byte* bytes, int byteCount, char* chars, int charCount) + { + this.LastCharCapacity = charCount; + this.DestinationWasNonNull = chars != null; + + if (charCount == 0) + { + return 0; + } + + chars[0] = (char)bytes[0]; + + return 1; + } + + public override int GetByteCount(char[] chars, int index, int count) + { + throw new NotSupportedException(); + } + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + { + throw new NotSupportedException(); + } + + public override int GetCharCount(byte[] bytes, int index, int count) + { + throw new NotSupportedException(); + } + + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + throw new NotSupportedException(); + } + } } From 879500f891a2dbd8b526938dc7d434394ec6bd3a Mon Sep 17 00:00:00 2001 From: Garume <80187947+Garume@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:12:40 +0900 Subject: [PATCH 2/2] Use a bounded inline stack buffer for short encoded StringPool inputs --- .../Buffers/StringPool.cs | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs b/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs index 9f6d2fd5d..5f645a9da 100644 --- a/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs +++ b/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs @@ -225,33 +225,21 @@ public unsafe string GetOrAdd(ReadOnlySpan span, Encoding encoding) if ((uint)maxLength <= 64) { - return GetOrAddWithStackBuffer(span, encoding, maxLength); - } - - using SpanOwner buffer = SpanOwner.Allocate(maxLength); + Span stackBuffer = stackalloc char[64]; - fixed (byte* source = span) - fixed (char* destination = &buffer.DangerousGetReference()) - { - int effectiveLength = encoding.GetChars(source, span.Length, destination, maxLength); + fixed (byte* source = span) + fixed (char* destination = stackBuffer) + { + int effectiveLength = encoding.GetChars(source, span.Length, destination, maxLength); - return GetOrAdd(new ReadOnlySpan(destination, effectiveLength)); + return GetOrAdd(new ReadOnlySpan(destination, effectiveLength)); + } } - } - /// - /// Decodes a bounded byte span using a stack-allocated buffer and returns its pooled string. - /// - /// The byte span to decode. - /// The encoding to use. - /// The maximum decoded character count. - /// The pooled string for the decoded characters. - private unsafe string GetOrAddWithStackBuffer(ReadOnlySpan span, Encoding encoding, int maxLength) - { - Span stackBuffer = stackalloc char[64]; + using SpanOwner buffer = SpanOwner.Allocate(maxLength); fixed (byte* source = span) - fixed (char* destination = stackBuffer) + fixed (char* destination = &buffer.DangerousGetReference()) { int effectiveLength = encoding.GetChars(source, span.Length, destination, maxLength);