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.

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

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:

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.

Get Exclusive Articles and Level-up Your C# Game Performance Optimizations in C#: 10 Best Practices