How to improve the performance of C# StringBuilder

This article explores best practices for using C# StringBuilder to reduce memory allocation and improve the performance of string operations.

In. NET, strings are immutable types. Whenever you modify a string object in. NET, a new string object will be created in memory to save new data. In contrast, the StringBuilder object represents a variable string and dynamically expands its memory allocation as the string size increases.

String and StringBuilder classes are two popular classes that you often use when processing strings in the. NET Framework and. NET Core. However, each class has its advantages and disadvantages.

BenchmarkDotNet is a lightweight open source library for benchmarking. NET code. BenchmarkDotNet can turn your methods into benchmarks, track them, and then provide insight into the captured performance data. In this article, we will benchmark our StringBuilder operations using BenchmarkDotNet.

To use the code examples provided in this article, you should have Visual Studio 2019 or above installed on your system.

1. Create a console application project in Visual Studio

First, let's create a. NET Core console application project in Visual Studio. Assuming that Visual Studio 2019 is already installed on your system, please follow the steps below to create a new. NET Core console application project.

  1. Start the Visual Studio IDE.
  2. Click "create new project".
  3. In the create new project window, select console application (. NET core) from the list of templates displayed.
  4. Click next.
  5. In the configure your new project window that appears next, specify the name and location of the new project.
  6. Click create.

This will create a new. NET Core console application project in Visual Studio 2019. We will use this project to deal with StringBuilder in subsequent chapters of this article.

2. Install the benchmark dotnet nuget package

To use BenchmarkDotNet, you must install the BenchmarkDotNet package. You can do this through the NuGet package manager in the Visual Studio 2019 IDE or by executing the following command on the NuGet package manager console.

Install-Package BenchmarkDotNet

3. Use StringBuilderCache to reduce allocation

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

StringBuilder cache works by caching a StringBuilder instance and then reusing it when a new StringBuilder instance is needed. This reduces allocation because you only need to have one instance of StringBuilder in memory.

Let's illustrate this with some code. Create a class named StringBuilderBenchmarkDemo in the Program.cs file. Create a method named AppendStringUsingStringBuilder. The code is as follows.

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

The code snippet above shows how to append strings using the StringBuilder object. Next, create a method named AppendStringUsingStringBuilderCache. The code is as follows.

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 shows how to use the Acquire method of the StringBuilderCache class to create a StringBuilder instance and then use it to append strings.

The following is 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 now use the BenchmarkRunner class to specify the initial starting point. This is a way to tell BenchmarkDotNet to run the benchmark on the specified class.

Replace the default source code for the Main method with the following code.

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

Now compile your project in Release mode and run the benchmark on the command line with the following command.

dotnet run -p StringBuilderPerfDemo.csproj -c Release

The performance differences between the two methods are described below.

As you can see, appending strings using StringBuilderCache is much faster and requires less allocation.

4. Use StringBuilder.AppendJoin instead of String.Join

String objects are immutable, so modifying a string object requires creating a new string object. Therefore, when connecting strings, you should use the StringBuilder.AppendJoin method instead of String.Join to reduce allocation and improve performance.

The following code list shows how to assemble a long string using the String.Join and StringBuilder.AppendJoin methods.

[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();
}

The following figure shows the benchmark results of these two methods.

Note that for this operation, the speed of the two methods is very close, but StringBuilder.AppendJoin uses significantly less memory.

5. Use StringBuilder to append a single character

Note that when using StringBuilder, if you need to append a single character, you should use Append(char) instead of Append(String).

Consider the following two approaches.

[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 you can see from the name, the AppendStringUsingString method explains how to Append a string using a string as a parameter of the Append method.

The AppendStringUsingChar method shows how you can use characters in the Append method to Append characters.

The following figure shows the benchmark results of these two methods.

6. Other StringBuilder optimization methods

StringBuilder allows you to set capacity to improve performance. If you know the size of the string you want to create, you can set the initial capacity accordingly to greatly reduce memory allocation.

You can also improve the performance of StringBuilder by using a reusable StringBuilder object pool to avoid allocation.

Finally, please note that since StringBuilderCache is an internal class, you need to paste the source code into your project to use it. Recall that in C #, you can only use one inner class in the same assembly or library.

Therefore, our program file cannot access the StringBuilderCache class only by referring to the library where the StringBuilderCache is located.

This is why we copy the source code of the StringBuilderCache class into our program file, that is, the Program.cs file.

reference material:

  1. C# tutorial
  2. C# programming technology
  3. Programming treasure house

Tags: C++ C#

Posted on Wed, 17 Nov 2021 22:02:47 -0500 by nathus