Some of the biggest performance problems in almost any .NET application boil down to string operations. They are both very common and by nature pretty expensive. In fact, looking at an average .NET Dump you’ll find that most of the memory is usually taken by strings (I heard about 70%).

As you probably know, strings are immutable. So whenever you concatenate strings, a new string object is allocated, populated with content, and eventually garbage collected. All of that is expensive and that’s why we (well, at least me) were taught that StringBuilder will always have better performance.

I tried to do some benchmarking to see if that’s really the case and was a little surprised by the results. Let’s see some benchmarks then.

NOTE: All the benchmarks are executed in Release (optimized) without a debugger attached. Each benchmark was executed 10 times and the displayed result is the always the average result. All measurements were done with the StopWatch class.

Benchmark 1: Single Expression Concatenation

Consider this code. And before reading forward, try guessing the result of this benchmark.

// These are randomly assigned at runtime and have about 10 chars each
public static string A { get; set; }
public static string B { get; set; }
public static string C { get; set; }
public static string D { get; set; }

private static string s;

public void ExecuteA()
{
    s = A + B + C + D;
}

public void ExecuteB()
{
    StringBuilder sb = new StringBuilder(A);
    sb.Append(B);
    sb.Append(C);
    sb.Append(D);
    s = sb.ToString();
}

public void ExecuteC()
{
    s = string.Format("{0}{1}{2}{3}", A,B,C,D);
}

public void ExecuteD()
{
    s = $"{A}{B}{C}{D}";
}

The result when executed 1,000,000 times is:

Execute A: 80.2267266666667ms (regular concatenation)
Execute B: 237.698413333333ms (StringBuilder)
Execute C: 260.183193333333ms (string.Format)
Execute D: 81.4275933333333ms (Interpolation)

Note that when changing the number of concatenations to about 15, StringBuilder becomes more efficient.

So several conclusions from this:

  1. StringBuilder doesn’t offer any advantages against single expression concatenations with a small number of strings.
  2. string.Format is the least performant from all available options. It’s pretty strange since as far as I know, interpolation is implemented with string.Format under the hood. So I tend to believe the compiler does optimizations specifically for interpolation with a small number of strings.
  3. When concatenating strings in a single expression, the compiler seems to do the same optimization as with string interpolation. This means there’s no advantage in using StringBuilder. Go with whatever is more readable.
  4. An interesting finding is that when the number of chars in the A,B,C, and D is small (1 char), the StringBuilder had almost the same performance as interpolation. I believe the reason is that in the scenario with 10 char strings we had to pay for its expansion in size.

Benchmark 2: Multi Expression Concatenation

ublic static string s;

public void ExecuteA()
{
    s = "";
    s += "a";
    s += "b";
    s += "c";
    s += "d";

}

public void ExecuteB()
{
    StringBuilder sb = new StringBuilder();
    sb.Append("a");
    sb.Append("b");
    sb.Append("c");
    sb.Append("d");
    s = sb.ToString();
}

public void ExecuteC()
{
    string s = "";
    for (int i = 0; i < 1000; i++)
    {
        s += "a";
    }
}

public void ExecuteD()
{
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 1000; i++)
    {
        sb.Append("a");
    }
}

Result are:

With 4 concatenations (executed 100,000 times):
Execute A: 8.84404666666667ms
Execute B: 6.10478666666667ms (StringBuilder)

With 1000 concatenations (executed 1,000 times):
Execute A: 313.65934ms
Execute B: 5.53542666666667ms (StringBuilder)

From executions A and B, we see the compiler doesn’t do its interpolation-like optimization, and new string objects are created. However, the result is still pretty similar to StringBuilder so we’re still losing on the allocation of the StringBuilder objects and method calls to .Append.

By the way, if the number of concatenations were 2 instead of 4, then StringBuilder would actually be less efficient than regular concatenation.

As expected, when the number of concatenation grows, the results change drastically. With 1000 operations, the StringBuilder is about 60 times faster than regular concatenation. So the usual paradigm that StringBuilder is always going to be more efficient with a large number of operations holds true.

Benchmark 3: Optimizing StringBuilder

When creating a StringBuilder, we’re getting the overhead of creating a new object. That object later needs to be garbage collected, which creates additional overhead. Consider the following benchmark:

public static string s;

private static StringBuilder sb = new StringBuilder();

public void ExecuteA()
{
    s = "";
    s += "a";
    s += "b";

}

public void ExecuteB()
{
    StringBuilder sb = new StringBuilder();
    sb.Append("a");
    sb.Append("b");
    s = sb.ToString();
}

public void ExecuteC()
{
    sb.Clear();
    sb.Append("a");
    sb.Append("b");
    s = sb.ToString();
}

The result when executing 1,000,000 times is:

Execute A: 33.1755533333333ms (concatenation)
Execute B: 48.07472ms (new StringBuilder())
Execute C: 33.6805466666667ms (reusing StringBuilder)

As you can see, just by reusing the same instance of the StringBuilder we improved the performance by almost 50%.

Admittedly, the performance difference is far less noticeable for many concatenations (or appends), so the use case should be very specific. In particular, it can be useful when you are using few appends and in a very high frequency. A classic case is for high-frequency logging.

Summary

Don’t know about you, but benchmarking is always fun. Here are my conclusions from this session:

  • Single-expression concatenations will have the best performance with regular concatenation or the string interpolation syntax.
  • For many concatenations, StringBuilder is still king.
  • The StringBuilder can be optimized by reusing the same instance and sb.Clear(). It’s most useful for small number of Appends.

A must disclaimer in any talk on performance is this:

Optimizing performance is not always necessary. In fact, mostly it’s negligible. In case of string manipulation, you’ll probably want to optimize only for algorithms and high-frequency operations. I’m talking in the ball park of millions of operations a second. Well, maybe less than that, but you get my meaning.