How to improve StringBuilder performance in C#

Take advantage of best practices for using StringBuilder to reduce memory allocations and improve the performance of your string operations.

Strings are immutable types in .NET. Whenever you modify a String object in .NET, a new String object is created in memory to hold the new data. By contrast, a StringBuilder object represents a mutable string of characters, and expands its memory allocation dynamically as the size of the string grows.

The String and StringBuilder classes are two popular classes that you will use frequently when working with strings in .NET Framework and in .NET Core. However, each has its benefits and downsides.

In an earlier post here, I discussed how these two classes compare and when one should be used in lieu of the other. In this article I’ll discuss how you can improve the performance of StringBuilder in C#.

BenchmarkDotNet is a lightweight, open source library for benchmarking .NET code. BenchmarkDotNet can transform your methods into benchmarks, track those methods, and then provide insights into the performance data captured. We’ll take advantage of BenchmarkDotNet to benchmark our StringBuilder operations in this post.

To work with the code examples provided in this article, you should have Visual Studio 2019 installed in your system. If you don’t already have a copy, you can download Visual Studio 2019 here.

Create a console application project in Visual Studio

First let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2019 is installed in your system, follow the steps outlined below to create a new .NET Core console application project.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window shown next, specify the name and location for the new project.
  6. Click Create.

This will create a new .NET Core console application project in Visual Studio 2019. We’ll use this project to work with StringBuilder in the subsequent sections of this article.

Install the BenchmarkDotNet NuGet package

To work with BenchmarkDotNet you must install the BenchmarkDotNet package. You can do this either via the NuGet Package Manager inside the Visual Studio 2019 IDE, or by executing the following command in the NuGet Package Manager Console:

Install-Package BenchmarkDotNet

Use StringBuilderCache to reduce allocations

StringBuilderCache is an internal class that is available in .NET and .NET Core. Whenever you have the need to create multiple instances of StringBuilder, you can use StringBuilderCache to reduce the cost of allocations considerably.

StringBuilderCache works by caching a StringBuilder instance and then reusing it when a new StringBuilder instance is needed. This reduces allocations because you need to have just one StringBuilder instance in the memory.

Let us illustrate this with some code. Create a class called StringBuilderBenchmarkDemo in the Program.cs file. Create a method named AppendStringUsingStringBuilder with the following code:

public string AppendStringUsingStringBuilder()
{
    var stringBuilder = new StringBuilder();
    stringBuilder.Append("First String");
    stringBuilder.Append("Second String");
    stringBuilder.Append("Third String");
    return stringBuilder.ToString();
}

The above code snippet shows how you can use a StringBuilder object to append strings. Next create a method called AppendStringUsingStringBuilderCache with the following code:

public string AppendStringUsingStringBuilderCache()
{
    var stringBuilder = StringBuilderCache.Acquire();
    stringBuilder.Append("First String");
    stringBuilder.Append("Second String");
    stringBuilder.Append("Third String");
    return StringBuilderCache.GetStringAndRelease(stringBuilder);
}

The above code snippet illustrates how you can create a StringBuilder instance using the Acquire method of the StringBuilderCache class and then use it to append strings.

Here’s the complete source code of the StringBuilderBenchmarkDemo class for your reference.

[MemoryDiagnoser]
public class StringBuilderBenchmarkDemo { [Benchmark]
      public string AppendStringUsingStringBuilder() {
            var stringBuilder = new StringBuilder();
            stringBuilder.Append("First String");
            stringBuilder.Append("Second String");
            stringBuilder.Append("Third String");
            return stringBuilder.ToString();
      }
      [Benchmark]
      public string AppendStringUsingStringBuilderCache() {
            var stringBuilder = StringBuilderCache.Acquire();
            stringBuilder.Append("First String");
            stringBuilder.Append("Second String");
            stringBuilder.Append("Third String");
            return StringBuilderCache.GetStringAndRelease(stringBuilder);
      }
}

You must specify the initial starting point now using the BenchmarkRunner class. This is a way of informing BenchmarkDotNet to run benchmarks on the class specified. Replace the default source code of the Main method using the following code:

static void Main(string[] args)
{
   var summary = BenchmarkRunner.Run<StringBuilderBenchmarkDemo>();
}

Now compile your project in Release mode and run benchmarking using the following command at the command line:

dotnet run -p StringBuilderPerfDemo.csproj -c Release

Figure 1 below illustrates the performance differences of the two methods.

stringbuilder csharp 01 IDG

Figure 1. Comparing StringBuilder performance with and without StringBuilderCache.

As you can see, appending strings using StringBuilderCache is much faster and needs fewer allocations.

Use StringBuilder.AppendJoin instead of String.Join

Recall that String objects are immutable, so modifying a String object requires the creation of a new String object. Thus you should use the StringBuilder.AppendJoin method in lieu of String.Join when concatenating strings to reduce allocations and improve performance.

The following code listing illustrates how you can use the String.Join and StringBuilder.AppendJoin methods to assemble a long string.

[Benchmark]
public string UsingStringJoin() {
            var list = new List < string > {
                        "A",
                        "B", "C", "D", "E"
            };
            var stringBuilder = new StringBuilder();
            for (int i = 0; i < 10000; i++) {
                        stringBuilder.Append(string.Join(' ', list));
            }
            return stringBuilder.ToString();
}
[Benchmark]
public string UsingAppendJoin() {
            var list = new List < string > {
                        "A",
                        "B", "C", "D", "E"
            };
            var stringBuilder = new StringBuilder();
            for (int i = 0; i < 10000; i++) {
                        stringBuilder.AppendJoin(' ', list);
            }
            return stringBuilder.ToString();
}

Figure 2 below displays the benchmark results of these two methods. Note that for this operation the two methods were close in speed, but StringBuilder.AppendJoin used significantly less memory.

stringbuilder csharp 02 IDG

Figure 2. Comparing String.Join and StringBuilder.AppendJoin.

Append a single character using StringBuilder

Note that when using StringBuilder, you should use Append(char) in lieu of Append(String) if you need to append a single character. Consider the following two methods:

[Benchmark]
public string AppendStringUsingString() {
      var stringBuilder = new StringBuilder();
      for (int i = 0; i < 1000; i++) {
            stringBuilder.Append("a");
            stringBuilder.Append("b");
            stringBuilder.Append("c");
      }
      return stringBuilder.ToString();
}
[Benchmark]
public string AppendStringUsingChar() {
      var stringBuilder = new StringBuilder();
      for (int i = 0; i < 1000; i++) {
            stringBuilder.Append('a');
            stringBuilder.Append('b');
            stringBuilder.Append('c');
      }
      return stringBuilder.ToString();
}

As evident from the name, the AppendStringUsingString method illustrates how you can append strings using a string as the parameter to the Append method. The AppendStringUsingChar method illustrates how you can use characters in the Append method to append characters. Figure 3 below shows the benchmarking result of the two methods.

stringbuilder csharp 03 IDG

Figure 3. Comparing Append(char) and Append(String) operations when using StringBuilder.

Other StringBuilder optimizations

StringBuilder allows you to set the capacity to increase performance. If you know the size of the string you’ll be creating, you can set the initial capacity accordingly to reduce memory allocation considerably.

You can also improve StringBuilder performance by using a reusable pool of StringBuilder objects to avoid allocations. These points were already discussed in an earlier article here.

Finally, note that, because StringBuilderCache is an internal class, you will need to paste the source code into your project to use it. Recall that you can use an internal class in C# within the same assembly or library only. Hence our program file cannot access the StringBuilderCache class simply by referencing the library in which StringBuilderCache is available. 

This is why we’ve copied the source code of the StringBuilderCache class into our program file, i.e., the Program.cs file.

Copyright © 2021 IDG Communications, Inc.

How to choose a low-code development platform