Skip to content

Commit

Permalink
Updated documentation and added more benchmarks
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Apr 8, 2022
1 parent 3263825 commit 952462e
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 10 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to **ValueStringBuilder** will be documented in this file. T

## [Unreleased]

### Added
- Added `Replace` methods which also tries to have the least amount of allocations.

## [0.9.2] - 2022-04-06

### Added
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,28 @@ The blog goes a bit more in detail how it works with a simplistic version of the

## What it doesn't solve!
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".
The library works best for a small to medium amount of strings (not multiple 100'000 characters, even though it is still faster and uses less allocations). At anytime you can convert the `ValueStringBuilder` to a "normal" `StringBuilder`.
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 less allocations). At anytime you can convert the `ValueStringBuilder` to a "normal" `StringBuilder` and vice versa.

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

## Documentation
A more detailed documentation can be found [here](https://linkdotnet.github.io/StringBuilder/).

## Benchmark

The following table gives you a small comparison between the `StringBuilder` which is part of .NET and the `ValueStringBuilder`:
The following table gives you a small comparison between the `StringBuilder` which is part of .NET, [`ZString`](https://github.com/Cysharp/ZString) and the `ValueStringBuilder`:

```no-class
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
| ------------------- | -------: | ------: | ------: | -----: | --------: |
| DotNetStringBuilder | 430.7 ns | 8.52 ns | 7.55 ns | 0.3576 | 1,496 B |
| ValueStringBuilder | 226.7 ns | 2.45 ns | 2.05 ns | 0.1395 | 584 B |
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated |
|-------------------- |-----------:|---------:|----------:|-----------:|------:|--------:|--------:|-------:|----------:|
| DotNetStringBuilder | 401.7 ns | 29.15 ns | 84.56 ns | 373.4 ns | 1.00 | 0.00 | 0.3576 | - | 1,496 B |
| ValueStringBuilder | 252.8 ns | 9.05 ns | 26.27 ns | 249.0 ns | 0.65 | 0.13 | 0.1583 | - | 664 B |
| ZStringBuilderUtf8 | 1,239.0 ns | 44.93 ns | 131.06 ns | 1,192.0 ns | 3.18 | 0.56 | 15.6250 | - | 66,136 B |
| ZStringBuilderUtf16 | 1,187.6 ns | 21.35 ns | 25.42 ns | 1,185.0 ns | 2.88 | 0.52 | 15.6250 | 0.0019 | 66,136 B |
```

For more comparison check the documentation.

Another benchmark shows that this `ValueStringBuilder` uses less memory when it comes to appending `ValueTypes` such as `int`, `double`, ...

```no-class
Expand Down
53 changes: 53 additions & 0 deletions docs/site/articles/comparison.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
uid: comparison
---

# Comparison

The following document will show some key differences between the `ValueStringBuilder` and similar working string builder like the one from .NET itself.

## System.Text.StringBuilder

The `StringBuilder` shipped with the .NET Framework itself is a all-purpose string builder which allows a versatile use. `ValueStringBuilder` tries to mimic the API as much as possible so developers can adopt the `ValueStringBuilder` easily where it makes sense. In the following part `StringBuilder` refers to `System.Text.StringBuilder`.

**Key differences**:
- `StringBuilder` is a class and does not have the restrictions coming with a `ref struct`. To know more head over to the [known limitations](xref:known_limitations) section.
- `StringBuilder` works not on `Span<T>` but more on `string`s or `char`s. Sometimes even with pointers
- `StringBuilder` uses chunks to represent the string, which the larger the string gets, the better it can perform. `ValueStringBuilder` only has one internal `Span` as representation which can cause fragmentation on very big strings.

## `ZString`
Both string builder use similiar concepts to achieve. Both,`ValueStringBuilder` and `ZString`, are declared as `struct`s. `ValueStringBuilder` goes one step further and enforces its lifecycle to live on the **stack** and can never be put on the **heap**.

**Key differences**:
* `ValueStringBuilder` is a `ref struct` which can never placed on the heap. `ZString` can be defined as a `class` field.
* `ZString` has a very big initial buffer in its default (64kb) which can lead to more pressure on the GC.
* `ZString` is more general purpose than `ValueStringBuilder` is.


## Benchmark

The following table gives you a small comparison between the `StringBuilder` which is part of .NET, [`ZString`](https://github.com/Cysharp/ZString) and the `ValueStringBuilder`:

```no-class
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated |
|-------------------- |-----------:|---------:|----------:|-----------:|------:|--------:|--------:|-------:|----------:|
| DotNetStringBuilder | 401.7 ns | 29.15 ns | 84.56 ns | 373.4 ns | 1.00 | 0.00 | 0.3576 | - | 1,496 B |
| ValueStringBuilder | 252.8 ns | 9.05 ns | 26.27 ns | 249.0 ns | 0.65 | 0.13 | 0.1583 | - | 664 B |
| ZStringBuilderUtf8 | 1,239.0 ns | 44.93 ns | 131.06 ns | 1,192.0 ns | 3.18 | 0.56 | 15.6250 | - | 66,136 B |
| ZStringBuilderUtf16 | 1,187.6 ns | 21.35 ns | 25.42 ns | 1,185.0 ns | 2.88 | 0.52 | 15.6250 | 0.0019 | 66,136 B |
```

For more comparison check the documentation.

Another benchmark shows that this `ValueStringBuilder` uses less memory when it comes to appending `ValueTypes` such as `int`, `double`, ...

```no-class
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
|-------------------- |---------:|---------:|---------:|--------:|-------:|----------:|
| DotNetStringBuilder | 16.31 us | 0.414 us | 1.208 us | 1.5259 | - | 6 KB |
| ValueStringBuilder | 14.61 us | 0.292 us | 0.480 us | 0.3357 | - | 1 KB |
| ZStringBuilder | 15.47 us | 0.249 us | 0.323 us | 16.1285 | 0.0153 | 67 KB |
```

Checkout the [Benchmark](https://github.com/linkdotnet/StringBuilder/tree/main/tests/LinkDotNet.StringBuilder.Benchmarks) for more detailed comparison and setup.
39 changes: 39 additions & 0 deletions docs/site/articles/getting_started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
uid: getting_started
---

# Getting started

The following section will show you how to use the [`ValueStringBuilder`](xref:LinkDotNet.StringBuilder.ValueStringBuilder).

For .NET 6 use the [nuget-package](https://www.nuget.org/packages/LinkDotNet.StringBuilder/):

> PM> Install-Package LinkDotNet.StringBuilder
Now that the package is installed the library can be used:

```csharp
using System;

using LinkDotNet.StringBuilder; // Namespace of the library
public static class Program
{
public static void Main()
{
var stringBuilder = new ValueStringBuilder();

stringBuilder.AppendLine("Hello World!");
stringBuilder.Append(0.3f);
stringBuilder.Insert(6, "dear ");
Console.WriteLine(stringBuilder.ToString());
}
}
```

Prints:

> Hello dear World!
0.3

[Here](https://dotnetfiddle.net/wM5r0q) an interactive example where you can fiddle around with the library. The example is hosted on [https://dotnetfiddle.net/](https://dotnetfiddle.net/) and already has the `ValueStringBuilder` nuget package included.
9 changes: 7 additions & 2 deletions docs/site/articles/toc.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
- name: How does it work?
href: concepts.md
- name: Getting started
href: getting_started.md
items:
- name: How does it work?
href: concepts.md
- name: Comparison
href: comparison.md
- name: Known limitations
href: known_limitations.md
2 changes: 2 additions & 0 deletions docs/site/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
- name: Api References
href: api/
homepage: api/index.md
- name: GitHub
href: https://github.com/linkdotnet/StringBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PackageTags>string,stringbuilder,csharp,dotnet</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>logo.png</PackageIcon>
<PackageVersion>0.9.2</PackageVersion>
<PackageVersion>0.9.3</PackageVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
27 changes: 26 additions & 1 deletion tests/LinkDotNet.StringBuilder.Benchmarks/AppendBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using BenchmarkDotNet.Attributes;
using Cysharp.Text;

namespace LinkDotNet.StringBuilder.Benchmarks;

[MemoryDiagnoser]
public class AppendBenchmarks
{
[Benchmark]
[Benchmark(Baseline = true)]
public string DotNetStringBuilder()
{
var builder = new System.Text.StringBuilder();
Expand All @@ -27,4 +28,28 @@ public string ValueStringBuilder()
builder.AppendLine("We can also add other Append method if we want. But we keep it easy for now.");
return builder.ToString();
}

[Benchmark]
public string ZStringBuilderUtf8()
{
var builder = ZString.CreateUtf8StringBuilder();
builder.AppendLine("That is the first line of our benchmark.");
builder.AppendLine("We can multiple stuff in here if want.");
builder.AppendLine("We can multiple stuff in here if want.");
builder.AppendLine("The idea is that we can resize the internal structure from time to time.");
builder.AppendLine("We can also add other Append method if we want. But we keep it easy for now.");
return builder.ToString();
}

[Benchmark]
public string ZStringBuilderUtf16()
{
var builder = ZString.CreateStringBuilder();
builder.AppendLine("That is the first line of our benchmark.");
builder.AppendLine("We can multiple stuff in here if want.");
builder.AppendLine("We can multiple stuff in here if want.");
builder.AppendLine("The idea is that we can resize the internal structure from time to time.");
builder.AppendLine("We can also add other Append method if we want. But we keep it easy for now.");
return builder.ToString();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using BenchmarkDotNet.Attributes;
using Cysharp.Text;

namespace LinkDotNet.StringBuilder.Benchmarks;

Expand Down Expand Up @@ -40,4 +41,22 @@ public string ValueStringBuilder()

return builder.ToString();
}

[Benchmark]
public string ZStringBuilder()
{
var builder = ZString.CreateStringBuilder();

for (var i = 0; i < 25; i++)
{
builder.Append(true);
builder.Append(int.MaxValue);
builder.Append(decimal.MaxValue);
builder.Append(byte.MinValue);
builder.Append(float.Epsilon);
builder.Append(double.Epsilon);
}

return builder.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="ZString" Version="2.4.4" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit 952462e

Please sign in to comment.