When to use String vs. StringBuilder in .NET Core

Take advantage of these best practices when working with strings in .NET Core for the optimal performance of your applications.

Two popular classes that you will use frequently when working with strings in .NET Core are the String and StringBuilder classes. You should be aware of the best practices when using both these classes to build applications that minimize allocations and are highly performant. This article discusses the best practices we can follow when working with strings in C#.

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. Note we’ll also use BenchmarkDotNet to track performance of the methods. If you’re not familiar with BenchmarkDotNet, I suggest reading this article first.

Benchmarking code is essential to understanding the performance of your application. It is always a good approach to have the performance metrics at hand when you’re optimizing code. The performance metrics will also help you to narrow in on the portions of the code in the application that need refactoring. In this article they will help us understand the performance of string operations in C#.

Create a .NET Core console application project in Visual Studio

First off, 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, specify the name and location for the new project.
  6. Click Create.

We’ll use this project to work with the String and StringBuilder classes 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

String concatenation using String and StringBuilder in C#

An immutable object is one that cannot be modified once it has been created. Since a string is an immutable data type in C#, when you combine two or more strings, a new string is produced.

However, whereas a string is an immutable type in C#, StringBuilder is an example of a mutable item. In C#, a StringBuilder is a mutable series of characters that can be extended to store more characters if required. Unlike with strings, modifying a StringBuilder instance does not result in the creation of a new instance in memory.

When you want to change a string, the Common Language Runtime generates a new string from scratch and discards the old one. So, if you append a series of characters to a string, you will recreate the same string in memory multiple times. By contrast, the StringBuilder class allocates memory for buffer space and then writes new characters directly into the buffer. Allocation happens only once.

Consider the following two methods:

[Benchmark]
public string StringConcatenationUsingStringBuilder()
        {
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < NumberOfRuns; i++)
            {
                stringBuilder.AppendLine("Hello World" + i);
            }
            return stringBuilder.ToString();
        }
[Benchmark]
public string StringConcatenationUsingString()
        {
            string str = null;
            for (int i = 0; i < NumberOfRuns; i++)
            {
                str += "Hello World" + i;
            }
            return str;
        }

Figure 1 shows the performance benchmarks of these two methods.

string vs stringbuilder 01 IDG

Figure 1.

As you can see in Figure 1, concatenating strings using StringBuilder is much faster, consumes much less memory, and uses fewer garbage collections in all generations, compared to using the ‘+’ operator to combine two or more strings.

Note that regular string concatenations are faster than using the StringBuilder but only when you’re using a few of them at a time. If you are using two or three string concatenations, use a string. StringBuilder will improve performance in cases where you make repeated modifications to a string or concatenate many strings together.

In short, use StringBuilder only for a large number of concatenations.

Reduce StringBuilder allocations using a reusable pool in C#

Consider the following two methods — one that creates StringBuilder instances without using a pool and another that creates StringBuilder instances using a reusable pool. By using a reusable pool, you can reduce allocations. When you need a StringBuilder instance, you can get one from the pool. When you’re done using the StringBuilder instance, you can return the instance back to the pool.

[Benchmark]
    public void CreateStringBuilderWithoutPool()
        {
            for (int i = 0; i < NumberOfRuns; i++)
            {
                var stringBuilder = new StringBuilder();
                stringBuilder.Append("Hello World" + i);
            }
        }
[Benchmark]
    public void CreateStringBuilderWithPool()
        {
            var stringBuilderPool = new
            DefaultObjectPoolProvider().CreateStringBuilderPool();
            for (var i = 0; i < NumberOfRuns; i++)
            {
                var stringBuilder = stringBuilderPool.Get();
                stringBuilder.Append("Hello World" + i);
                stringBuilderPool.Return(stringBuilder);
            }
        }

Figure 2 shows the performance benchmarks of these two methods.

string vs stringbuilder 02 IDG

Figure 2.

As you can see in Figure 2, the memory allocation decreases considerably when you use a reusable pool.

Extract strings using Substring vs. Append in C#

Let us now compare the performance of the Substring method of the String class vs. the Append method of the StringBuilder class for extracting a string from another string.

Consider the following piece of code that illustrates two methods — one that extracts a string from another string using the Substring method of the String class and one that does the same using the Append method of the StringBuilder class.

[Benchmark]
    public string ExtractStringUsingSubstring()
        {
            const string str = "This is a sample text";
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < NumberOfRuns; i++)
            {
                stringBuilder.Append(str.Substring(0, 10));
            }
            return stringBuilder.ToString();
        }
[Benchmark]
    public string ExtractStringUsingAppend()
        {
            const string str = "This is a sample text";
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < NumberOfRuns; i++)
            {
                stringBuilder.Append(str, 0, 10);
            }
            return stringBuilder.ToString();
        }

Figure 3 shows the performance benchmarks of these two methods.

string vs stringbuilder 03 IDG

Figure 3.

As you can see in Figure 3, using the Append method of the StringBuilder class to extract a string is both faster and consumes fewer resources than using Substring.

Join strings using String.Join vs. StringBuilder.AppendJoin in C#

When you’re joining strings, use StringBuilder.AppendJoin in lieu of String.Join for reduced allocations. Consider the following code snippet that illustrates two methods — one that joins strings using String.Join and the other that does the same using StringBuilder.AppendJoin.

[Benchmark]
    public string JoinStringsUsingStringJoin()
        {
            var stringBuilder = new StringBuilder();
            for (int i = 0; i < NumberOfRuns; i++)
            {
                stringBuilder.Append(string.Join("Hello", ' ', "World"));
            }
            return stringBuilder.ToString();
        }
[Benchmark]
    public string JoinStringsUsingAppendJoin()
        {
            var stringBuilder = new StringBuilder();
            for (int i = 0; i < NumberOfRuns; i++)
            {
                stringBuilder.AppendJoin("Hello", ' ', "World");
            }
            return stringBuilder.ToString();
        }

Figure 4 shows the performance benchmarks of these two methods.

string vs stringbuilder 04 IDG

Figure 4.

As when extracting a string from a string, StringBuilder offers advantages over the String class when joining strings. As you can see in Figure 4, using AppendJoin of the StringBuilder class is again faster and more resource efficient than using Join of the String class.

When using StringBuilder, you can also set the capacity of the StringBuilder instance to improve performance. If you know the size of the string you will be creating, you can set the initial capacity when creating a StringBuilder instance. This can reduce the memory allocation considerably.

On a final note, the String.Create method is yet another way to improve string handling performance in .NET Core. It provides an efficient way to create strings at runtime. I’ll discuss String.Create in a future post here.

Copyright © 2021 IDG Communications, Inc.