From c69b317ccedaded8b34536c25b3dd1a21af102c4 Mon Sep 17 00:00:00 2001 From: Joyless <65855333+Joy-less@users.noreply.github.com> Date: Fri, 24 Jan 2025 03:19:07 +0000 Subject: [PATCH 1/3] Optimize EnsureCapacity --- .../IntegerSpanList.cs | 4 +- .../ValueStringBuilder.Append.cs | 10 +-- .../ValueStringBuilder.Helper.cs | 14 ++++ .../ValueStringBuilder.Insert.cs | 4 +- .../ValueStringBuilder.cs | 73 ++++++++----------- .../ValueStringBuilderExtensions.cs | 2 +- .../ValueStringBuilderExtensionsTests.cs | 2 +- 7 files changed, 54 insertions(+), 55 deletions(-) create mode 100644 src/LinkDotNet.StringBuilder/ValueStringBuilder.Helper.cs diff --git a/src/LinkDotNet.StringBuilder/IntegerSpanList.cs b/src/LinkDotNet.StringBuilder/IntegerSpanList.cs index 578b42c..632e354 100644 --- a/src/LinkDotNet.StringBuilder/IntegerSpanList.cs +++ b/src/LinkDotNet.StringBuilder/IntegerSpanList.cs @@ -31,7 +31,7 @@ public void Add(int value) { if (count >= buffer.Length) { - Grow(); + EnsureCapacity(); } buffer[count] = value; @@ -39,7 +39,7 @@ public void Add(int value) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Grow(int capacity = 0) + private void EnsureCapacity(int capacity = 0) { var currentSize = buffer.Length; var newSize = capacity > 0 ? capacity : currentSize * 2; diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.Append.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.Append.cs index d126ea8..fb4c692 100644 --- a/src/LinkDotNet.StringBuilder/ValueStringBuilder.Append.cs +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilder.Append.cs @@ -20,7 +20,7 @@ public unsafe void Append(bool value) if (newSize > buffer.Length) { - Grow(newSize); + EnsureCapacity(newSize); } fixed (char* dest = &buffer[bufferPosition]) @@ -67,7 +67,7 @@ public void Append(scoped ReadOnlySpan str) var newSize = str.Length + bufferPosition; if (newSize > buffer.Length) { - Grow(newSize); + EnsureCapacity(newSize); } ref var strRef = ref MemoryMarshal.GetReference(str); @@ -111,7 +111,7 @@ public void Append(char value) var newSize = bufferPosition + 1; if (newSize > buffer.Length) { - Grow(newSize); + EnsureCapacity(newSize); } buffer[bufferPosition] = value; @@ -163,7 +163,7 @@ public Span AppendSpan(int length) var origPos = bufferPosition; if (origPos > buffer.Length - length) { - Grow(length); + EnsureCapacity(length); } bufferPosition = origPos + length; @@ -177,7 +177,7 @@ private void AppendSpanFormattable(T value, ReadOnlySpan format = defau var newSize = bufferSize + bufferPosition; if (newSize >= Capacity) { - Grow(newSize); + EnsureCapacity(newSize); } if (!value.TryFormat(buffer[bufferPosition..], out var written, format, null)) diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.Helper.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.Helper.cs new file mode 100644 index 0000000..c5cc310 --- /dev/null +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilder.Helper.cs @@ -0,0 +1,14 @@ +namespace LinkDotNet.StringBuilder; + +public ref partial struct ValueStringBuilder +{ + /// + /// Finds the smallest power of 2 which is greater than or equal to . + /// + /// The value the result should be greater than or equal to. + /// The smallest power of 2 >= . + internal static int FindSmallestPowerOf2Above(int minimum) + { + return 1 << (int)Math.Ceiling(Math.Log2(minimum)); + } +} \ No newline at end of file diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.Insert.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.Insert.cs index a6cc015..2fa946f 100644 --- a/src/LinkDotNet.StringBuilder/ValueStringBuilder.Insert.cs +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilder.Insert.cs @@ -45,7 +45,7 @@ public void Insert(int index, scoped ReadOnlySpan value) var newLength = bufferPosition + value.Length; if (newLength > buffer.Length) { - Grow(newLength); + EnsureCapacity(newLength); } bufferPosition = newLength; @@ -79,7 +79,7 @@ private void InsertSpanFormattable(int index, T value, scoped ReadOnlySpan buffer.Length) { - Grow(newLength); + EnsureCapacity(newLength); } bufferPosition = newLength; diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs index 1e8468d..89a8cdc 100644 --- a/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs @@ -20,15 +20,12 @@ namespace LinkDotNet.StringBuilder; private char[]? arrayFromPool; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct using a rented buffer of capacity 32. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public ValueStringBuilder() { - bufferPosition = 0; - buffer = default; - arrayFromPool = null; - Grow(32); + EnsureCapacity(32); } /// @@ -41,9 +38,7 @@ public ValueStringBuilder() #endif public ValueStringBuilder(Span initialBuffer) { - bufferPosition = 0; buffer = initialBuffer; - arrayFromPool = null; } /// @@ -63,7 +58,7 @@ public ValueStringBuilder(ReadOnlySpan initialText) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ValueStringBuilder(int initialCapacity) { - Grow(initialCapacity); + EnsureCapacity(initialCapacity); } /// @@ -173,19 +168,41 @@ public readonly string ToString(Range range) public void Clear() => bufferPosition = 0; /// - /// Ensures that the builder has at least amount of capacity. + /// Ensures the builder's buffer size is at least , renting a larger buffer if not. /// /// New capacity for the builder. /// - /// If is smaller or equal to nothing will be done. + /// If is already >= , nothing is done. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void EnsureCapacity(int newCapacity) { - if (newCapacity > Length) + if (Length >= newCapacity) { - Grow(newCapacity); + return; } + + int newSize = FindSmallestPowerOf2Above(newCapacity); + + Span rented = ArrayPool.Shared.Rent(newSize); + + if (bufferPosition > 0) + { + ref char sourceRef = ref MemoryMarshal.GetReference(buffer); + ref char destinationRef = ref MemoryMarshal.GetReference(rented); + + Unsafe.CopyBlock( + ref Unsafe.As(ref destinationRef), + ref Unsafe.As(ref sourceRef), + (uint)bufferPosition * sizeof(char)); + } + + if (arrayFromPool is not null) + { + ArrayPool.Shared.Return(arrayFromPool); + } + + buffer = rented; } /// @@ -301,36 +318,4 @@ public void Dispose() /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void Reverse() => buffer[..bufferPosition].Reverse(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Grow(int capacity = 0) - { - var size = buffer.Length == 0 ? 8 : buffer.Length; - - while (size < capacity) - { - size *= 2; - } - - var rented = ArrayPool.Shared.Rent(size); - - if (bufferPosition > 0) - { - ref var sourceRef = ref MemoryMarshal.GetReference(buffer); - ref var destinationRef = ref MemoryMarshal.GetReference(rented.AsSpan()); - - Unsafe.CopyBlock( - ref Unsafe.As(ref destinationRef), - ref Unsafe.As(ref sourceRef), - (uint)(bufferPosition * sizeof(char))); - } - - var oldBufferFromPool = arrayFromPool; - buffer = arrayFromPool = rented; - - if (oldBufferFromPool is not null) - { - ArrayPool.Shared.Return(oldBufferFromPool); - } - } } \ No newline at end of file diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilderExtensions.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilderExtensions.cs index 7a05e07..4487bbe 100644 --- a/src/LinkDotNet.StringBuilder/ValueStringBuilderExtensions.cs +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilderExtensions.cs @@ -25,7 +25,7 @@ public static System.Text.StringBuilder ToStringBuilder(this ValueStringBuilder /// The builder from which the new instance is derived. /// A new instance with the string represented by this builder. /// Throws if is null. - public static ValueStringBuilder ToValueStringBuilder(this System.Text.StringBuilder? builder) + public static ValueStringBuilder ToValueStringBuilder(this System.Text.StringBuilder builder) { ArgumentNullException.ThrowIfNull(builder); diff --git a/tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderExtensionsTests.cs b/tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderExtensionsTests.cs index b1f28f8..972e219 100644 --- a/tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderExtensionsTests.cs +++ b/tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderExtensionsTests.cs @@ -31,7 +31,7 @@ public void ShouldThrowWhenStringBuilderNull() { System.Text.StringBuilder? sb = null; - Action act = () => sb.ToValueStringBuilder(); + Action act = () => sb!.ToValueStringBuilder(); act.ShouldThrow(); } From fa3bc0f052117375abf418f0b4d9dd84a0b0d2da Mon Sep 17 00:00:00 2001 From: Joyless <65855333+Joy-less@users.noreply.github.com> Date: Fri, 24 Jan 2025 03:42:38 +0000 Subject: [PATCH 2/3] Fix optimization not returning to pool --- src/LinkDotNet.StringBuilder/ValueStringBuilder.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs index 89a8cdc..e40fafa 100644 --- a/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs @@ -184,12 +184,12 @@ public void EnsureCapacity(int newCapacity) int newSize = FindSmallestPowerOf2Above(newCapacity); - Span rented = ArrayPool.Shared.Rent(newSize); + char[] rented = ArrayPool.Shared.Rent(newSize); if (bufferPosition > 0) { ref char sourceRef = ref MemoryMarshal.GetReference(buffer); - ref char destinationRef = ref MemoryMarshal.GetReference(rented); + ref char destinationRef = ref MemoryMarshal.GetReference(rented.AsSpan()); Unsafe.CopyBlock( ref Unsafe.As(ref destinationRef), @@ -203,6 +203,7 @@ ref Unsafe.As(ref sourceRef), } buffer = rented; + arrayFromPool = rented; } /// From 8bcec5192570a486193798660e679d9726740923 Mon Sep 17 00:00:00 2001 From: Joyless <65855333+Joy-less@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:38:20 +0000 Subject: [PATCH 3/3] Move EnsureCapacity to its own file --- .../ValueStringBuilder.EnsureCapacity.cs | 57 +++++++++++++++++++ .../ValueStringBuilder.Helper.cs | 14 ----- .../ValueStringBuilder.cs | 39 ------------- 3 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 src/LinkDotNet.StringBuilder/ValueStringBuilder.EnsureCapacity.cs delete mode 100644 src/LinkDotNet.StringBuilder/ValueStringBuilder.Helper.cs diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.EnsureCapacity.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.EnsureCapacity.cs new file mode 100644 index 0000000..f8c0c4a --- /dev/null +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilder.EnsureCapacity.cs @@ -0,0 +1,57 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LinkDotNet.StringBuilder; + +public ref partial struct ValueStringBuilder +{ + /// + /// Ensures the builder's buffer size is at least , renting a larger buffer if not. + /// + /// New capacity for the builder. + /// + /// If is already >= , nothing is done. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void EnsureCapacity(int newCapacity) + { + if (Length >= newCapacity) + { + return; + } + + var newSize = FindSmallestPowerOf2Above(newCapacity); + + var rented = ArrayPool.Shared.Rent(newSize); + + if (bufferPosition > 0) + { + ref var sourceRef = ref MemoryMarshal.GetReference(buffer); + ref var destinationRef = ref MemoryMarshal.GetReference(rented.AsSpan()); + + Unsafe.CopyBlock( + ref Unsafe.As(ref destinationRef), + ref Unsafe.As(ref sourceRef), + (uint)bufferPosition * sizeof(char)); + } + + if (arrayFromPool is not null) + { + ArrayPool.Shared.Return(arrayFromPool); + } + + buffer = rented; + arrayFromPool = rented; + } + + /// + /// Finds the smallest power of 2 which is greater than or equal to . + /// + /// The value the result should be greater than or equal to. + /// The smallest power of 2 >= . + private static int FindSmallestPowerOf2Above(int minimum) + { + return 1 << (int)Math.Ceiling(Math.Log2(minimum)); + } +} \ No newline at end of file diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.Helper.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.Helper.cs deleted file mode 100644 index c5cc310..0000000 --- a/src/LinkDotNet.StringBuilder/ValueStringBuilder.Helper.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace LinkDotNet.StringBuilder; - -public ref partial struct ValueStringBuilder -{ - /// - /// Finds the smallest power of 2 which is greater than or equal to . - /// - /// The value the result should be greater than or equal to. - /// The smallest power of 2 >= . - internal static int FindSmallestPowerOf2Above(int minimum) - { - return 1 << (int)Math.Ceiling(Math.Log2(minimum)); - } -} \ No newline at end of file diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs index e40fafa..dfcc688 100644 --- a/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs @@ -167,45 +167,6 @@ public readonly string ToString(Range range) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() => bufferPosition = 0; - /// - /// Ensures the builder's buffer size is at least , renting a larger buffer if not. - /// - /// New capacity for the builder. - /// - /// If is already >= , nothing is done. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void EnsureCapacity(int newCapacity) - { - if (Length >= newCapacity) - { - return; - } - - int newSize = FindSmallestPowerOf2Above(newCapacity); - - char[] rented = ArrayPool.Shared.Rent(newSize); - - if (bufferPosition > 0) - { - ref char sourceRef = ref MemoryMarshal.GetReference(buffer); - ref char destinationRef = ref MemoryMarshal.GetReference(rented.AsSpan()); - - Unsafe.CopyBlock( - ref Unsafe.As(ref destinationRef), - ref Unsafe.As(ref sourceRef), - (uint)bufferPosition * sizeof(char)); - } - - if (arrayFromPool is not null) - { - ArrayPool.Shared.Return(arrayFromPool); - } - - buffer = rented; - arrayFromPool = rented; - } - /// /// Removes a range of characters from this builder. ///