Skip to content

Commit 4b481b0

Browse files
author
LinkDotNet Bot
committed
Updating to newest release
2 parents 6a503cc + cec5468 commit 4b481b0

24 files changed

+124
-131
lines changed

CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ All notable changes to **ValueStringBuilder** will be documented in this file. T
66

77
## [Unreleased]
88

9+
## [2.4.1] - 2025-03-25
10+
11+
### Changed
12+
13+
- Optimized `Replace(char, char)` (by @Joy-less in #241)
14+
- Optimized `Replace(ReadOnlySpan<char>, ReadOnlySpan<char>)` when both spans are length 1 (by @Joy-less in #241)
15+
916
## [2.4.0] - 2025-02-21
1017

1118
### Added
@@ -489,7 +496,8 @@ This release brings extensions to the `ValueStringBuilder` API. For `v1.0` the `
489496

490497
- Initial release
491498

492-
[unreleased]: https://github.com/linkdotnet/StringBuilder/compare/2.4.0...HEAD
499+
[unreleased]: https://github.com/linkdotnet/StringBuilder/compare/2.4.1...HEAD
500+
[2.4.1]: https://github.com/linkdotnet/StringBuilder/compare/2.4.0...2.4.1
493501
[2.4.0]: https://github.com/linkdotnet/StringBuilder/compare/2.3.1...2.4.0
494502
[2.3.1]: https://github.com/linkdotnet/StringBuilder/compare/2.3.0...2.3.1
495503
[2.3.0]: https://github.com/linkdotnet/StringBuilder/compare/2.2.0...2.3.0

Directory.Build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
33
<ItemGroup>
4-
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.6.0.109712">
4+
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
55
<PrivateAssets>all</PrivateAssets>
66
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
77
</PackageReference>

LinkDotNet.StringBuilder.sln

-53
This file was deleted.

LinkDotNet.StringBuilder.slnx

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Solution>
2+
<Folder Name="/.text/">
3+
<File Path="CHANGELOG.md" />
4+
<File Path="README.md" />
5+
</Folder>
6+
<Folder Name="/.workflows/">
7+
<File Path=".github/dependabot.yml" />
8+
<File Path=".github/workflows/codeql.yml" />
9+
<File Path=".github/workflows/create-release.yml" />
10+
<File Path=".github/workflows/docs.yml" />
11+
<File Path=".github/workflows/dotnet.yml" />
12+
<File Path=".github/workflows/update-release.yml" />
13+
</Folder>
14+
<Folder Name="/src/">
15+
<Project Path="src/LinkDotNet.StringBuilder/LinkDotNet.StringBuilder.csproj" />
16+
</Folder>
17+
<Folder Name="/tests/">
18+
<Project Path="tests/LinkDotNet.StringBuilder.Benchmarks/LinkDotNet.StringBuilder.Benchmarks.csproj" />
19+
<Project Path="tests/LinkDotNet.StringBuilder.UnitTests/LinkDotNet.StringBuilder.UnitTests.csproj" />
20+
</Folder>
21+
</Solution>

README.md

+32-28
Original file line numberDiff line numberDiff line change
@@ -14,66 +14,70 @@ Afterward, use the package as follow:
1414
```csharp
1515
using LinkDotNet.StringBuilder; // Namespace of the package
1616
17-
ValueStringBuilder stringBuilder = new ValueStringBuilder();
17+
using ValueStringBuilder stringBuilder = new();
1818
stringBuilder.AppendLine("Hello World");
1919

2020
string result = stringBuilder.ToString();
2121
```
2222

2323
There are also smaller helper functions, which enable you to use `ValueStringBuilder` without any instance:
2424
```csharp
25-
using LinkDotNet.StringBuilder;
25+
string result1 = ValueStringBuilder.Concat("Hello ", "World"); // "Hello World"
26+
string result2 = ValueStringBuilder.Concat("Hello", 1, 2, 3, "!"); // "Hello123!"
27+
```
2628

27-
_ = ValueStringBuilder.Concat("Hello ", "World"); // "Hello World"
28-
_ = ValueStringBuilder.Concat("Hello", 1, 2, 3, "!"); // "Hello123!"
29+
By default, `ValueStringBuilder` uses a rented buffer from `ArrayPool<char>.Shared`.
30+
You can avoid renting overhead with an initially stack-allocated buffer:
31+
```csharp
32+
using ValueStringBuilder stringBuilder = new(stackalloc char[128]);
2933
```
34+
Note that this will prevent you from returning `stringBuilder` or assigning it to an `out` parameter.
3035

3136
## What does it solve?
3237
The dotnet version of the `StringBuilder` is an all-purpose version that normally fits a wide variety of needs.
3338
But sometimes, low allocation is key. Therefore I created the `ValueStringBuilder`. It is not a class but a `ref struct` that tries to allocate as little as possible.
3439
If you want to know how the `ValueStringBuilder` works and why it uses allocations and is even faster, check out [this](https://steven-giesel.com/blogPost/4cada9a7-c462-4133-ad7f-e8b671987896) blog post.
3540
The blog goes into a bit more in detail about how it works with a simplistic version of the `ValueStringBuilder`.
3641

37-
## What it doesn't solve!
38-
The library is not meant as a general replacement for the `StringBuilder` shipped with the .net framework itself. You can head over to the documentation and read about the ["Known limitations"](https://linkdotnet.github.io/StringBuilder/articles/known_limitations.html).
39-
The library works best for a small to medium amount of strings (not multiple 100'000 characters, even though it can be still faster and uses fewer allocations). At any time, you can convert the `ValueStringBuilder` to a "normal" `StringBuilder` and vice versa.
42+
## What doesn't it solve?
43+
The library is not meant as a general replacement for the `StringBuilder` built into .NET. You can head over to the documentation and read about the ["Known limitations"](https://linkdotnet.github.io/StringBuilder/articles/known_limitations.html).
44+
The library works best for a small to medium length strings (not hundreds of thousands of characters, even though it can be still faster and performs fewer allocations). At any time, you can convert the `ValueStringBuilder` to a "normal" `StringBuilder` and vice versa.
4045

4146
The normal use case is to concatenate strings in a hot path where the goal is to put as minimal pressure on the GC as possible.
4247

4348
## Documentation
44-
More detailed documentation can be found [here](https://linkdotnet.github.io/StringBuilder/). It is really important to understand how the `ValueStringBuilder` works so that you did not run into weird situations where performance/allocations can even rise.
49+
More detailed documentation can be found [here](https://linkdotnet.github.io/StringBuilder). It is really important to understand how the `ValueStringBuilder` works so that you did not run into weird situations where performance/allocations can even rise.
4550

4651
## Benchmark
4752

48-
The following table gives you a small comparison between the `StringBuilder` which is part of .NET and the `ValueStringBuilder`:
53+
The following table compares the built-in `StringBuilder` and this library's `ValueStringBuilder`:
4954

5055
```no-class
51-
BenchmarkDotNet=v0.13.2, OS=macOS Monterey 12.6.1 (21G217) [Darwin 21.6.0]
52-
Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores
53-
.NET SDK=7.0.100-rc.2.22477.23
54-
[Host] : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
55-
DefaultJob : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
56-
57-
58-
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
59-
|------------------------------- |-----------:|---------:|---------:|------:|--------:|--------:|----------:|------------:|
60-
| DotNetStringBuilder | 227.3 ns | 1.31 ns | 1.22 ns | 1.00 | 0.00 | 0.7114 | 1488 B | 1.00 |
61-
| ValueStringBuilder | 128.7 ns | 0.57 ns | 0.53 ns | 0.57 | 0.00 | 0.2677 | 560 B | 0.38 |
62-
| ValueStringBuilderPreAllocated | 113.9 ns | 0.67 ns | 0.60 ns | 0.50 | 0.00 | 0.2677 | 560 B | 0.38 |
56+
BenchmarkDotNet v0.14.0, macOS Sequoia 15.3.1 (24D70) [Darwin 24.3.0]
57+
Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores
58+
.NET SDK 9.0.200
59+
[Host] : .NET 9.0.2 (9.0.225.6610), Arm64 RyuJIT AdvSIMD
60+
DefaultJob : .NET 9.0.2 (9.0.225.6610), Arm64 RyuJIT AdvSIMD
61+
62+
63+
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
64+
|-------------------- |----------:|---------:|---------:|------:|-------:|----------:|------------:|
65+
| DotNetStringBuilder | 126.74 ns | 0.714 ns | 0.667 ns | 1.00 | 0.1779 | 1488 B | 1.00 |
66+
| ValueStringBuilder | 95.69 ns | 0.118 ns | 0.110 ns | 0.76 | 0.0669 | 560 B | 0.38 |
6367
```
6468

65-
For more comparison check the documentation.
69+
For more comparisons, check the documentation.
6670

67-
Another benchmark shows that this `ValueStringBuilder` uses less memory when it comes to appending `ValueTypes` such as `int`, `double`, ...
71+
Another benchmark shows that `ValueStringBuilder` allocates less memory when appending value types (such as `int` and `double`):
6872

6973
```no-class
70-
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
71-
|-------------------- |---------:|---------:|---------:|-------:|----------:|
72-
| DotNetStringBuilder | 17.21 us | 0.622 us | 1.805 us | 1.5259 | 6 KB |
73-
| ValueStringBuilder | 16.24 us | 0.496 us | 1.462 us | 0.3357 | 1 KB |
74+
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
75+
|------------------------------- |---------:|--------:|--------:|-------:|-------:|----------:|
76+
| ValueStringBuilderAppendFormat | 821.7 ns | 1.29 ns | 1.14 ns | 0.4330 | - | 3.54 KB |
77+
| StringBuilderAppendFormat | 741.5 ns | 5.58 ns | 5.22 ns | 0.9909 | 0.0057 | 8.1 KB |
7478
```
7579

76-
Checkout the [Benchmark](tests/LinkDotNet.StringBuilder.Benchmarks) for a more detailed comparison and setup.
80+
Check out the [Benchmark](tests/LinkDotNet.StringBuilder.Benchmarks) for a more detailed comparison and setup.
7781

7882
## Support & Contributing
7983

docs/site/articles/comparison.md

+15-8
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@ The `StringBuilder` shipped with the .NET Framework itself is a all-purpose stri
2222
The following table gives you a small comparison between the `StringBuilder` which is part of .NET and the `ValueStringBuilder`:
2323

2424
```
25-
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated |
26-
|-------------------- |-----------:|---------:|----------:|-----------:|------:|--------:|--------:|-------:|----------:|
27-
| DotNetStringBuilder | 401.7 ns | 29.15 ns | 84.56 ns | 373.4 ns | 1.00 | 0.00 | 0.3576 | - | 1,496 B |
28-
| ValueStringBuilder | 252.8 ns | 9.05 ns | 26.27 ns | 249.0 ns | 0.65 | 0.13 | 0.1583 | - | 664 B |
25+
BenchmarkDotNet v0.14.0, macOS Sequoia 15.3.1 (24D70) [Darwin 24.3.0]
26+
Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores
27+
.NET SDK 9.0.200
28+
[Host] : .NET 9.0.2 (9.0.225.6610), Arm64 RyuJIT AdvSIMD
29+
DefaultJob : .NET 9.0.2 (9.0.225.6610), Arm64 RyuJIT AdvSIMD
30+
31+
32+
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
33+
|-------------------- |----------:|---------:|---------:|------:|-------:|----------:|------------:|
34+
| DotNetStringBuilder | 126.74 ns | 0.714 ns | 0.667 ns | 1.00 | 0.1779 | 1488 B | 1.00 |
35+
| ValueStringBuilder | 95.69 ns | 0.118 ns | 0.110 ns | 0.76 | 0.0669 | 560 B | 0.38 |
2936
```
3037

3138
For more comparison check the documentation.
@@ -34,10 +41,10 @@ Another benchmark shows that this `ValueStringBuilder` uses less memory when it
3441

3542

3643
```
37-
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
38-
|-------------------- |---------:|---------:|---------:|--------:|-------:|----------:|
39-
| DotNetStringBuilder | 16.31 us | 0.414 us | 1.208 us | 1.5259 | - | 6 KB |
40-
| ValueStringBuilder | 14.61 us | 0.292 us | 0.480 us | 0.3357 | - | 1 KB |
44+
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
45+
|------------------------------- |---------:|--------:|--------:|-------:|-------:|----------:|
46+
| ValueStringBuilderAppendFormat | 821.7 ns | 1.29 ns | 1.14 ns | 0.4330 | - | 3.54 KB |
47+
| StringBuilderAppendFormat | 741.5 ns | 5.58 ns | 5.22 ns | 0.9909 | 0.0057 | 8.1 KB |
4148
4249
```
4350

src/LinkDotNet.StringBuilder/LinkDotNet.StringBuilder.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
</PropertyGroup>
4343

4444
<ItemGroup>
45-
<PackageReference Include="Meziantou.Analyzer" Version="2.0.186">
45+
<PackageReference Include="Meziantou.Analyzer" Version="2.0.189">
4646
<PrivateAssets>all</PrivateAssets>
4747
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4848
</PackageReference>

src/LinkDotNet.StringBuilder/ValueStringBuilder.AppendFormat.cs

+1-3
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,7 @@ public void AppendFormat<T1, T2, T3, T4, T5>(
362362
[MethodImpl(MethodImplOptions.AggressiveInlining)]
363363
private static int GetValidArgumentIndex(ReadOnlySpan<char> placeholder, int allowedRange)
364364
{
365-
#pragma warning disable MA0011
366-
if (!int.TryParse(placeholder[1..^1], out var argIndex))
367-
#pragma warning restore MA0011
365+
if (!int.TryParse(placeholder[1..^1], null, out var argIndex))
368366
{
369367
throw new FormatException("Invalid argument index in format string: " + placeholder.ToString());
370368
}

src/LinkDotNet.StringBuilder/ValueStringBuilder.Concat.Helper.cs

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static string Concat<T>(params T[] values)
2020

2121
using var sb = new ValueStringBuilder(stackalloc char[128]);
2222
sb.AppendJoin(string.Empty, values);
23+
2324
return sb.ToString();
2425
}
2526

src/LinkDotNet.StringBuilder/ValueStringBuilder.EnsureCapacity.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ ref Unsafe.As<char, byte>(ref sourceRef),
4949
/// Finds the smallest power of 2 which is greater than or equal to <paramref name="minimum"/>.
5050
/// </summary>
5151
/// <param name="minimum">The value the result should be greater than or equal to.</param>
52-
/// <returns>The smallest power of 2 >= <paramref name="minimum"/>.</returns>
52+
/// <returns>The smallest power of 2 &gt;= <paramref name="minimum"/>.</returns>
53+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5354
private static int FindSmallestPowerOf2Above(int minimum)
5455
{
5556
return 1 << (int)Math.Ceiling(Math.Log2(minimum));

src/LinkDotNet.StringBuilder/ValueStringBuilder.Enumerator.cs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public ref partial struct ValueStringBuilder
99
/// Creates an enumerator over the characters in the builder.
1010
/// </summary>
1111
/// <returns>An enumerator over the characters in the builder.</returns>
12+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1213
public readonly Enumerator GetEnumerator() => new(buffer[..bufferPosition]);
1314

1415
/// <summary>Enumerates the elements of a <see cref="Span{T}"/>.</summary>

src/LinkDotNet.StringBuilder/ValueStringBuilder.Pad.cs

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Runtime.CompilerServices;
2+
13
namespace LinkDotNet.StringBuilder;
24

35
public ref partial struct ValueStringBuilder
@@ -7,6 +9,7 @@ public ref partial struct ValueStringBuilder
79
/// </summary>
810
/// <param name="totalWidth">Total width of the string after padding.</param>
911
/// <param name="paddingChar">Character to pad the string with.</param>
12+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1013
public void PadLeft(int totalWidth, char paddingChar)
1114
{
1215
if (totalWidth <= bufferPosition)
@@ -27,6 +30,7 @@ public void PadLeft(int totalWidth, char paddingChar)
2730
/// </summary>
2831
/// <param name="totalWidth">Total width of the string after padding.</param>
2932
/// <param name="paddingChar">Character to pad the string with.</param>
33+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3034
public void PadRight(int totalWidth, char paddingChar)
3135
{
3236
if (totalWidth <= bufferPosition)

src/LinkDotNet.StringBuilder/ValueStringBuilder.Replace.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,7 @@ public readonly void Replace(char oldValue, char newValue, int startIndex, int c
2626
ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0);
2727
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex + count, Length);
2828

29-
for (var i = startIndex; i < startIndex + count; i++)
30-
{
31-
if (buffer[i] == oldValue)
32-
{
33-
buffer[i] = newValue;
34-
}
35-
}
29+
buffer.Slice(startIndex, count).Replace(oldValue, newValue);
3630
}
3731

3832
/// <summary>
@@ -97,6 +91,12 @@ public void Replace(scoped ReadOnlySpan<char> oldValue, scoped ReadOnlySpan<char
9791
return;
9892
}
9993

94+
if (oldValue.Length == 1 && newValue.Length == 1)
95+
{
96+
Replace(oldValue[0], newValue[0], startIndex, count);
97+
return;
98+
}
99+
100100
var index = startIndex;
101101
var remainingChars = count;
102102

0 commit comments

Comments
 (0)