From 8358c419d7aeeb9bba314f94950afe529b477f5f Mon Sep 17 00:00:00 2001 From: Rickard Date: Mon, 1 Jul 2024 23:36:27 +0200 Subject: [PATCH 1/4] Use var-int size --- Performance/Program.cs | 4 +- .../TestSupport.cs | 17 +++++ .../Test_Allocation.cs | 32 ++++++---- .../Test_Deduplication.cs | 6 +- .../Test_Multiple.cs | 6 +- src/Combination.StringPools/Utf8StringPool.cs | 64 +++++++++++++++++-- 6 files changed, 100 insertions(+), 29 deletions(-) create mode 100644 src/Combination.StringPools.Tests/TestSupport.cs diff --git a/Performance/Program.cs b/Performance/Program.cs index dd87c98..fcb54dc 100644 --- a/Performance/Program.cs +++ b/Performance/Program.cs @@ -4,8 +4,8 @@ using Combination.StringPools; using Performance; -//var summary = BenchmarkRunner.Run(); -var summary = BenchmarkRunner.Run(); +var summary = BenchmarkRunner.Run(); +//var summary = BenchmarkRunner.Run(); #if false foreach (var sizeMultiple in Enumerable.Range(1, 11)) diff --git a/src/Combination.StringPools.Tests/TestSupport.cs b/src/Combination.StringPools.Tests/TestSupport.cs new file mode 100644 index 0000000..fceb9fd --- /dev/null +++ b/src/Combination.StringPools.Tests/TestSupport.cs @@ -0,0 +1,17 @@ +using System.Numerics; + +public static class TestSupport +{ + public static int GetAllocationSize(int length) => length + 1 + (BitOperations.Log2((uint)length) / 7); + public static int GetMaxStringSizeForAllocation(int allocationSize) + { + for (var i = allocationSize - 1; i >= 0; --i) + { + if (GetAllocationSize(i) == allocationSize) + { + return i; + } + } + throw new ArgumentOutOfRangeException("allocationSize"); + } +} diff --git a/src/Combination.StringPools.Tests/Test_Allocation.cs b/src/Combination.StringPools.Tests/Test_Allocation.cs index ac8c3a4..0d169e1 100644 --- a/src/Combination.StringPools.Tests/Test_Allocation.cs +++ b/src/Combination.StringPools.Tests/Test_Allocation.cs @@ -50,7 +50,7 @@ public void Add_String_Smaller_Than_Page_Succeeds(int pageSize) using var pool = StringPool.Utf8(pageSize, 1); Assert.Equal(0, pool.UsedBytes); Assert.Equal(pageSize, pool.AllocatedBytes); - var someString = new string('c', pageSize - 2); + var someString = new string('c', TestSupport.GetMaxStringSizeForAllocation(pageSize)); var pooledString = pool.Add(someString); Assert.Equal(pageSize, pool.UsedBytes); Assert.Equal(pageSize, pool.AllocatedBytes); @@ -69,7 +69,7 @@ public void Add_String_Larger_Than_Page_Fails(int pageSize) using var pool = StringPool.Utf8(pageSize, 1); Assert.Equal(0, pool.UsedBytes); Assert.Equal(pageSize, pool.AllocatedBytes); - var someString = new string('c', pageSize - 1); + var someString = new string('c', TestSupport.GetMaxStringSizeForAllocation(pageSize) + 1); var exception = Assert.Throws(() => pool.Add(someString)); Assert.Contains("String is too long to be pooled", exception.Message); } @@ -86,13 +86,13 @@ public void Two_Strings_Fit_Same_Page(int pageSize) using var pool = StringPool.Utf8(pageSize, 1); Assert.Equal(0, pool.UsedBytes); Assert.Equal(pageSize, pool.AllocatedBytes); - var string1 = new string('c', (pageSize / 2) - 2); + var string1 = new string('c', TestSupport.GetMaxStringSizeForAllocation(pageSize / 2)); var pooledString1 = pool.Add(string1); Assert.Equal(pageSize / 2, pool.UsedBytes); Assert.Equal(pageSize, pool.AllocatedBytes); - var string2 = new string('d', pageSize - (pageSize / 2) - 2); + var string2 = new string('d', TestSupport.GetMaxStringSizeForAllocation(pageSize / 2)); var pooledString2 = pool.Add(string2); - Assert.Equal(pageSize, pool.UsedBytes); + Assert.InRange(pool.UsedBytes, pageSize - 1, pageSize); Assert.Equal(pageSize, pool.AllocatedBytes); Assert.Equal(string1, (string)pooledString1); Assert.Equal(string2, (string)pooledString2); @@ -107,17 +107,17 @@ public void Two_Strings_Fit_Same_Page(int pageSize) [InlineData(65535)] public void Two_Strings_Dont_Fit_Need_More_Space(int pageSize) { - var stringSize = (pageSize / 2) - 1; + var stringSize = TestSupport.GetMaxStringSizeForAllocation(pageSize / 2) + 1; using var pool = StringPool.Utf8(pageSize, 1); Assert.Equal(0, pool.UsedBytes); Assert.Equal(pageSize, pool.AllocatedBytes); var string1 = new string('c', stringSize); var pooledString1 = pool.Add(string1); - Assert.Equal(stringSize + 2, pool.UsedBytes); + Assert.Equal(TestSupport.GetAllocationSize(stringSize), pool.UsedBytes); Assert.Equal(pageSize, pool.AllocatedBytes); var string2 = new string('d', stringSize); var pooledString2 = pool.Add(string2); - Assert.Equal((2 * stringSize) + 4, pool.UsedBytes); + Assert.Equal(2 * TestSupport.GetAllocationSize(stringSize), pool.UsedBytes); Assert.Equal(2 * pageSize, pool.AllocatedBytes); Assert.Equal(string1, (string)pooledString1); Assert.Equal(stringSize, pooledString1.Length); @@ -161,8 +161,10 @@ public void Dispose_Thread_Safe(int numThreads, int numPages) { Interlocked.Increment(ref numDisposed); } - }); - t.Priority = ThreadPriority.AboveNormal; + }) + { + Priority = ThreadPriority.AboveNormal + }; t.Start(); threads.Add(t); } @@ -191,7 +193,7 @@ public void Successive_Sizes(int stringSize) } } - public static IEnumerable Sizes = Enumerable.Range(0, 14).Select(x => new object[] { x }); + public static readonly IEnumerable Sizes = Enumerable.Range(0, 14).Select(x => new object[] { x }); [Theory] [InlineData(2, 1)] @@ -213,7 +215,7 @@ public void Add_Deduplicated_Thread_Safe(int numThreads, int numPages) { try { - for (var i = 0;; ++i) + for (var i = 0; ; ++i) { var str = pool.Add("foobar " + ((seed + i) % 1000)); Interlocked.Add(ref stringSum, str.ToString().Length); @@ -227,8 +229,10 @@ public void Add_Deduplicated_Thread_Safe(int numThreads, int numPages) { Interlocked.Increment(ref numDisposed); } - }); - t.Priority = ThreadPriority.AboveNormal; + }) + { + Priority = ThreadPriority.AboveNormal + }; t.Start(); threads.Add(t); } diff --git a/src/Combination.StringPools.Tests/Test_Deduplication.cs b/src/Combination.StringPools.Tests/Test_Deduplication.cs index 97a4531..c0be4ca 100644 --- a/src/Combination.StringPools.Tests/Test_Deduplication.cs +++ b/src/Combination.StringPools.Tests/Test_Deduplication.cs @@ -18,7 +18,7 @@ public void Equal_Strings_Deduplicated(int numStrings, int numUniqueStrings, int pool.Add(someString); } - Assert.Equal(((stringSize * 2) + 2) * numUniqueStrings, pool.UsedBytes); + Assert.Equal(TestSupport.GetAllocationSize(stringSize * 2) * numUniqueStrings, pool.UsedBytes); for (var i = 0; i < numUniqueStrings * 2; ++i) { var someString = new string(Convert.ToChar('ä' + i), stringSize); @@ -46,7 +46,7 @@ public void Equal_Strings_Deduplicated_Bytes(int numStrings, int numUniqueString pool.Add(bytes); } - Assert.Equal(((stringSize * 2) + 2) * numUniqueStrings, pool.UsedBytes); + Assert.Equal(TestSupport.GetAllocationSize(stringSize * 2) * numUniqueStrings, pool.UsedBytes); for (var i = 0; i < numUniqueStrings * 2; ++i) { var someString = new string(Convert.ToChar('ä' + i), stringSize); @@ -102,6 +102,6 @@ public void Equal_Strings_Deduplicated_Thread_Safe(int numThreads, int numString t.Join(); } - Assert.Equal(10 * (2 + stringSize), pool.UsedBytes); + Assert.Equal(10 * TestSupport.GetAllocationSize(stringSize), pool.UsedBytes); } } diff --git a/src/Combination.StringPools.Tests/Test_Multiple.cs b/src/Combination.StringPools.Tests/Test_Multiple.cs index bdbd4ad..ab19bfc 100644 --- a/src/Combination.StringPools.Tests/Test_Multiple.cs +++ b/src/Combination.StringPools.Tests/Test_Multiple.cs @@ -69,7 +69,7 @@ public void Multiple_Pools_Kept_Separate(int numPools, int stringsPerPool, int s foreach (var pool in pools) { - Assert.Equal((stringSize + 2) * stringsPerPool, pool.UsedBytes); + Assert.Equal(TestSupport.GetAllocationSize(stringSize) * stringsPerPool, pool.UsedBytes); pool.Dispose(); Assert.Equal(0, pool.UsedBytes); } @@ -94,7 +94,7 @@ public void Multiple_Pools_Kept_Separate_Thread_Safe(int numThreads, int strings Assert.Same(pool, str.StringPool); } - Assert.Equal(stringsPerPool * (2 + stringSize), pool.UsedBytes); + Assert.Equal(stringsPerPool * TestSupport.GetAllocationSize(stringSize), pool.UsedBytes); }); t.Start(); threads.Add(t); @@ -125,7 +125,7 @@ public void Multiple_Deduplicated_Pools_Kept_Separate_Thread_Safe(int numThreads Assert.Same(pool, str.StringPool); } - Assert.Equal(10 * (2 + stringSize), pool.UsedBytes); + Assert.Equal(10 * TestSupport.GetAllocationSize(stringSize), pool.UsedBytes); }); t.Start(); threads.Add(t); diff --git a/src/Combination.StringPools/Utf8StringPool.cs b/src/Combination.StringPools/Utf8StringPool.cs index f1fdf55..b4fff3d 100644 --- a/src/Combination.StringPools/Utf8StringPool.cs +++ b/src/Combination.StringPools/Utf8StringPool.cs @@ -1,3 +1,4 @@ +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -90,6 +91,9 @@ PooledUtf8String IUtf8StringPool.Add(ReadOnlySpan value) PooledUtf8String IUtf8StringPool.Add(ReadOnlySpan value) => AddInternal(value); + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static int GetAllocationSize(int length) => length + 1 + (BitOperations.Log2((uint)length) / 7); + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private PooledUtf8String AddInternal(ReadOnlySpan value) @@ -100,8 +104,8 @@ private PooledUtf8String AddInternal(ReadOnlySpan value) return PooledUtf8String.Empty; } - var structLength = length + 2; - if (structLength > 0xffff || structLength > pageSize) + var structLength = GetAllocationSize(length); + if (structLength > pageSize) { throw new ArgumentOutOfRangeException(nameof(value), "String is too long to be pooled"); } @@ -163,8 +167,24 @@ private PooledUtf8String AddInternal(ReadOnlySpan value) Interlocked.Add(ref usedBytes, structLength); unsafe { - *(ushort*)(writePtr + pageStartOffset) = checked((ushort)length); - var stringWritePtr = new Span((byte*)(writePtr + pageStartOffset + 2), length); + var ptr = (byte*)(writePtr + pageStartOffset); + var write = length; + while (true) + { + if (write > 0x7f) + { + *ptr++ = unchecked((byte)(0x80 | (write & 0x7f))); + } + else + { + *ptr++ = unchecked((byte)write); + break; + } + + write >>= 7; + } + + var stringWritePtr = new Span(ptr, length); value.CopyTo(stringWritePtr); } @@ -347,8 +367,21 @@ private ReadOnlySpan GetStringBytes(ulong offset) unsafe { - var length = ((ushort*)(pages[page] + pageOffset))[0]; - return new ReadOnlySpan((byte*)(pages[page] + pageOffset + 2), length); + var ptr = (byte*)(pages[page] + pageOffset); + var length = 0; + var shl = 0; + while (true) + { + var t = *ptr++; + length += (t & 0x7f) << shl; + shl += 7; + if ((t & 0x80) == 0) + { + break; + } + } + + return new ReadOnlySpan(ptr, length); } } @@ -397,7 +430,24 @@ private int GetStringLength(ulong offset) throw new ArgumentOutOfRangeException(nameof(offset), $"Invalid handle value, page {page} is out of range 0..{pages.Count}"); } - return unchecked((ushort)Marshal.ReadInt16(pages[page] + pageOffset)); + unsafe + { + var ptr = (byte*)(pages[page] + pageOffset); + var length = 0; + var shl = 0; + while (true) + { + var t = *ptr++; + length += (t & 0x7f) << shl; + shl += 7; + if ((t & 0x80) == 0) + { + break; + } + } + + return length; + } } } From a79775a80280c01d4797cbc551418e4619a24640 Mon Sep 17 00:00:00 2001 From: Rickard Date: Tue, 2 Jul 2024 09:48:22 +0200 Subject: [PATCH 2/4] Update Program.cs --- Performance/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Performance/Program.cs b/Performance/Program.cs index fcb54dc..dd87c98 100644 --- a/Performance/Program.cs +++ b/Performance/Program.cs @@ -4,8 +4,8 @@ using Combination.StringPools; using Performance; -var summary = BenchmarkRunner.Run(); -//var summary = BenchmarkRunner.Run(); +//var summary = BenchmarkRunner.Run(); +var summary = BenchmarkRunner.Run(); #if false foreach (var sizeMultiple in Enumerable.Range(1, 11)) From 3d13c8dfb74c66d77ef98eff86f1bc8a6d722211 Mon Sep 17 00:00:00 2001 From: Rickard Date: Wed, 3 Jul 2024 02:13:52 +0200 Subject: [PATCH 3/4] Migrated test --- src/Combination.StringPools.Tests/Test_Allocation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Combination.StringPools.Tests/Test_Allocation.cs b/src/Combination.StringPools.Tests/Test_Allocation.cs index 81c0c01..7606144 100644 --- a/src/Combination.StringPools.Tests/Test_Allocation.cs +++ b/src/Combination.StringPools.Tests/Test_Allocation.cs @@ -300,7 +300,7 @@ public void Add_Deduplicated_Thread_Safe(int numThreads, int numPages) var sum = 0L; for (var i = 0; i < 1000; ++i) { - var len = 2 + ("foobar " + i).Length; + var len = TestSupport.GetAllocationSize(("foobar " + i).Length); sum += len; } From a2ec301f02caae46bc57a1e118e2e6859f931305 Mon Sep 17 00:00:00 2001 From: Rickard Date: Wed, 3 Jul 2024 08:43:05 +0200 Subject: [PATCH 4/4] Migrated test --- src/Combination.StringPools.Tests/Test_Allocation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Combination.StringPools.Tests/Test_Allocation.cs b/src/Combination.StringPools.Tests/Test_Allocation.cs index 7606144..9912f7d 100644 --- a/src/Combination.StringPools.Tests/Test_Allocation.cs +++ b/src/Combination.StringPools.Tests/Test_Allocation.cs @@ -269,7 +269,7 @@ public void Add_Deduplicated_Thread_Safe(int numThreads, int numPages) for (var i = 0; !stopped; ++i) { var str = pool.Add("foobar " + (i % 1000)); - Interlocked.Add(ref stringSum, 2 + str.ToString().Length); + Interlocked.Add(ref stringSum, TestSupport.GetAllocationSize(str.ToString().Length)); if (i == 10000) { Interlocked.Increment(ref numStarted);