Maybe don't use StringBuilder

So, should you use StringBuilder? Yes, sometimes. It depends on what your data is like, and what you're optimizing for.

Maybe don't use StringBuilder

I was originally going to title this article "Stop using StringBuilder." However, once I actually started doing these performance tests, I decided it's not so easy to say that StringBuilder is rarely warranted. I've always read that C# string concatenation is very efficient, and StringBuilder is generally not necessary for small tasks. So when I found myself in a code review session having to approve a change that only replaced 15ish string concatenations with a StringBuilder, I was annoyed. But, I didn't have solid evidence that this was unnecessary so I set out to prove that StringBuilder is pointless at the scale I was looking at. And I was probably right, but it turns out it's not as simple as that.

Benchmarking

First things, first: how did I perform these benchmarks? The bulk of the answer is by using BenchmarkDotNet which is purpose built for exactly what I'm trying to do. I enabled memory diagnostics so that I could compare memory usage of the methods in addition to simple execution time. Otherwise, I used the default configuration. I ran the benchmark in DotNetCore 2.1 on my laptop (a Lenovo Thinkpad X1 Carbon with an i7-8550U CPU @ 1.8GHz). It's medium powered as far as ultrabooks go, but these numbers should really only be compared to each other. You should not try to extrapolate absolute performance on other hardware from here, although I would expect relative performance to stay pretty consistent. At a guess, CPU cache would have the biggest performance impact from hardware, and would benefit concatenation the most.

The central question I wanted to answer was: at what point is StringBuilder more efficient than string concatenation? But, as long as I was testing things, I thought I would add in String.Format and string interpolation just to see how much worse they are. I began by writing some very simple benchmark functions for each method, and then doing some sample runs. What I discovered right away is that StringBuilder obliterates every other method when you're doing hundreds of appends or more. In fact, I was honestly shocked by how much more efficient it was and it made me wonder if using longer strings would have an effect. Early tests were just appending strings of 2 or 3 characters at a time. I added a second append with a variable length (up to 20 characters in my test) to see what affect that had, and it evened things out a little bit, especially when doing only few total appends. However, that made the results less obvious as to what was happening, so I then wanted to remake the test functions so that each iteration performed only one append. The other thing I discovered quickly is that String.Format and string interpolation have nearly identical performance in terms of speed and memory use. So I dropped String.Format from later tests. And I was also surprised by how bad the performance is for interpolation. It made me wonder how String.Join performs. It performs great, as it turn out. Almost on par with StringBuilder. Below is the final design of the test functions.

Test Functions

public string Concat(int n, string l)
{
    var result = "";
    for (int i = 0; i < n; i++)
    {
        result += l;
    }

    return result;
}

public string Interpolation(int n, string l)
{
    var result = "";
    for (int i = 0; i < n; i++)
    {
        result = $"{result}{l}";
    }

    return result;
}

public string StringBuilder(int n, string l)
{
    StringBuilder sb = new StringBuilder("");
    for (int i = 0; i < n; i++)
    {
        sb.Append(l);
    }

    return sb.ToString();
}

public string Join(int n, string l)
{
    var list = Enumerable.Range(0, n).Select(i => l);
    return String.Join("", list);
}

The source is available on Github if you want to check it out for yourself and see everything in context.

Results

Method N L Mean StdDev Median Ratio Rank Allocated
Concat 2 2 16.00 ns 0.2878 ns 15.92 ns 1.00 1 40 B
Interpolation 2 2 17.53 ns 0.1672 ns 17.56 ns 1.10 2 40 B
Join 2 2 184.93 ns 2.8090 ns 183.70 ns 11.57 4 224 B
StringBuilder 2 2 30.52 ns 0.5233 ns 30.42 ns 1.90 3 144 B
Concat 2 5 16.33 ns 0.1830 ns 16.27 ns 1.00 1 48 B
Interpolation 2 5 16.33 ns 0.1457 ns 16.33 ns 1.00 1 48 B
Join 2 5 185.34 ns 1.1699 ns 185.18 ns 11.36 3 232 B
StringBuilder 2 5 34.84 ns 0.8521 ns 34.87 ns 2.14 2 152 B
Concat 2 10 19.23 ns 0.6165 ns 19.43 ns 1.00 2 72 B
Interpolation 2 10 18.18 ns 0.1635 ns 18.16 ns 0.96 1 72 B
Join 2 10 196.21 ns 8.0639 ns 194.07 ns 10.50 4 256 B
StringBuilder 2 10 73.43 ns 0.6432 ns 73.50 ns 3.89 3 280 B
Concat 2 20 20.97 ns 1.3874 ns 20.57 ns 1.00 1 112 B
Interpolation 2 20 20.92 ns 0.3753 ns 21.01 ns 0.99 1 112 B
Join 2 20 195.32 ns 4.4982 ns 194.63 ns 9.38 3 296 B
StringBuilder 2 20 102.27 ns 0.8764 ns 102.00 ns 4.81 2 456 B
Concat 5 2 66.11 ns 1.4388 ns 65.99 ns 1.00 3 176 B
Interpolation 5 2 61.14 ns 1.0574 ns 61.20 ns 0.93 2 176 B
Join 5 2 240.45 ns 1.9389 ns 239.78 ns 3.64 4 232 B
StringBuilder 5 2 40.85 ns 0.7670 ns 40.61 ns 0.62 1 152 B
Concat 5 5 65.82 ns 2.2897 ns 65.10 ns 1.00 1 256 B
Interpolation 5 5 65.75 ns 1.3523 ns 65.63 ns 0.99 1 256 B
Join 5 5 260.47 ns 2.8452 ns 260.15 ns 3.95 3 264 B
StringBuilder 5 5 85.25 ns 1.4721 ns 85.19 ns 1.29 2 288 B
Concat 5 10 71.99 ns 0.8786 ns 72.02 ns 1.00 1 400 B
Interpolation 5 10 74.75 ns 2.8402 ns 74.14 ns 1.04 2 400 B
Join 5 10 267.96 ns 3.3458 ns 268.25 ns 3.72 4 312 B
StringBuilder 5 10 134.92 ns 2.7072 ns 134.77 ns 1.87 3 472 B
Concat 5 20 88.44 ns 1.5302 ns 88.47 ns 1.00 1 688 B
Interpolation 5 20 86.96 ns 1.4053 ns 86.70 ns 0.98 1 688 B
Join 5 20 272.00 ns 6.2162 ns 272.71 ns 3.08 3 416 B
StringBuilder 5 20 165.19 ns 1.6099 ns 164.81 ns 1.87 2 776 B
Concat 10 2 144.31 ns 2.4661 ns 144.07 ns 1.00 3 488 B
Interpolation 10 2 135.77 ns 1.4342 ns 135.78 ns 0.94 2 488 B
Join 10 2 336.31 ns 4.6052 ns 334.75 ns 2.33 4 256 B
StringBuilder 10 2 101.38 ns 1.9880 ns 101.83 ns 0.71 1 280 B
Concat 10 5 166.82 ns 2.0832 ns 166.40 ns 1.00 2 800 B
Interpolation 10 5 160.51 ns 8.5764 ns 157.86 ns 0.99 1 800 B
Join 10 5 369.39 ns 7.8767 ns 366.89 ns 2.22 3 312 B
StringBuilder 10 5 161.63 ns 13.9031 ns 158.17 ns 0.90 1 472 B
Concat 10 10 202.29 ns 8.6888 ns 201.52 ns 1.00 2 1352 B
Interpolation 10 10 200.44 ns 1.8474 ns 200.50 ns 1.01 2 1352 B
Join 10 10 369.50 ns 3.3972 ns 368.74 ns 1.87 3 416 B
StringBuilder 10 10 193.00 ns 2.1110 ns 192.48 ns 0.98 1 776 B
Concat 10 20 253.53 ns 2.2917 ns 253.15 ns 1.00 1 2448 B
Interpolation 10 20 248.03 ns 3.3504 ns 247.82 ns 0.98 1 2448 B
Join 10 20 415.70 ns 5.9607 ns 412.21 ns 1.64 2 616 B
StringBuilder 10 20 249.46 ns 3.0857 ns 250.30 ns 0.98 1 1304 B
Concat 20 2 326.53 ns 3.4100 ns 327.20 ns 1.00 3 1408 B
Interpolation 20 2 312.66 ns 8.3133 ns 311.07 ns 0.96 2 1408 B
Join 20 2 511.66 ns 2.4801 ns 511.72 ns 1.57 4 296 B
StringBuilder 20 2 173.51 ns 2.4892 ns 173.64 ns 0.53 1 456 B
Concat 20 5 365.91 ns 2.7664 ns 365.43 ns 1.00 2 2640 B
Interpolation 20 5 366.99 ns 1.6394 ns 367.32 ns 1.00 2 2640 B
Join 20 5 610.68 ns 18.1561 ns 615.95 ns 1.64 3 416 B
StringBuilder 20 5 241.82 ns 5.2586 ns 241.88 ns 0.66 1 776 B
Concat 20 10 506.42 ns 14.0452 ns 505.09 ns 1.00 2 4752 B
Interpolation 20 10 503.22 ns 8.0601 ns 503.91 ns 0.98 2 4752 B
Join 20 10 584.42 ns 2.6017 ns 585.29 ns 1.14 3 616 B
StringBuilder 20 10 297.74 ns 3.1423 ns 297.24 ns 0.58 1 1304 B
Concat 20 20 728.42 ns 14.9323 ns 724.75 ns 1.00 2 8968 B
Interpolation 20 20 766.76 ns 18.9677 ns 766.67 ns 1.05 3 8968 B
Join 20 20 926.47 ns 7.7350 ns 930.46 ns 1.27 4 2472 B
StringBuilder 20 20 410.23 ns 7.7488 ns 409.03 ns 0.56 1 2288 B
Concat 40 2 728.42 ns 11.9854 ns 727.20 ns 1.00 2 4448 B
Interpolation 40 2 722.30 ns 7.2398 ns 721.21 ns 0.99 2 4448 B
Join 40 2 904.93 ns 9.2397 ns 904.94 ns 1.24 3 376 B
StringBuilder 40 2 294.75 ns 3.1231 ns 294.49 ns 0.40 1 736 B
Concat 40 5 1,027.28 ns 18.9552 ns 1,022.69 ns 1.00 2 9320 B
Interpolation 40 5 1,074.60 ns 12.1262 ns 1,074.47 ns 1.05 3 9320 B
Join 40 5 1,013.56 ns 23.8777 ns 1,007.30 ns 0.98 2 616 B
StringBuilder 40 5 421.94 ns 4.4232 ns 423.00 ns 0.41 1 1304 B
Concat 40 10 1,495.01 ns 12.6292 ns 1,496.65 ns 1.00 3 17552 B
Interpolation 40 10 1,557.12 ns 16.8712 ns 1,561.67 ns 1.04 4 17552 B
Join 40 10 1,356.94 ns 19.7533 ns 1,357.23 ns 0.91 2 2472 B
StringBuilder 40 10 549.20 ns 6.0773 ns 550.03 ns 0.37 1 2288 B
Concat 40 20 2,567.70 ns 33.8659 ns 2,566.94 ns 1.00 3 34008 B
Interpolation 40 20 2,668.41 ns 18.2871 ns 2,663.05 ns 1.04 4 34008 B
Join 40 20 1,415.40 ns 10.9350 ns 1,414.92 ns 0.55 2 4368 B
StringBuilder 40 20 620.53 ns 4.0165 ns 620.03 ns 0.24 1 4184 B
Concat 80 2 1,869.03 ns 22.8140 ns 1,862.40 ns 1.00 3 15328 B
Interpolation 80 2 1,971.99 ns 22.4544 ns 1,981.26 ns 1.06 4 15328 B
Join 80 2 1,729.19 ns 19.7028 ns 1,722.37 ns 0.93 2 536 B
StringBuilder 80 2 533.75 ns 6.0512 ns 534.77 ns 0.29 1 1224 B
Concat 80 5 3,076.35 ns 50.9248 ns 3,072.86 ns 1.00 4 34680 B
Interpolation 80 5 2,987.44 ns 38.2992 ns 2,976.91 ns 0.97 3 34680 B
Join 80 5 2,091.04 ns 14.7741 ns 2,090.59 ns 0.68 2 2472 B
StringBuilder 80 5 705.76 ns 2.6973 ns 705.69 ns 0.23 1 2288 B
Concat 80 10 4,975.15 ns 41.5071 ns 4,964.11 ns 1.00 3 67152 B
Interpolation 80 10 4,935.99 ns 53.4951 ns 4,928.61 ns 0.99 3 67152 B
Join 80 10 2,293.88 ns 31.5881 ns 2,290.50 ns 0.46 2 4368 B
StringBuilder 80 10 868.58 ns 14.0799 ns 865.18 ns 0.17 1 4184 B
Concat 80 20 8,943.60 ns 139.3530 ns 8,933.98 ns 1.00 3 132088 B
Interpolation 80 20 9,054.01 ns 106.4972 ns 9,036.53 ns 1.01 3 132088 B
Join 80 20 2,569.69 ns 35.7711 ns 2,566.84 ns 0.29 2 8088 B
StringBuilder 80 20 1,196.11 ns 20.6975 ns 1,191.40 ns 0.13 1 7904 B
Concat 160 2 5,293.69 ns 55.2972 ns 5,282.14 ns 1.00 3 56288 B
Interpolation 160 2 5,231.26 ns 35.8164 ns 5,236.22 ns 0.99 3 56288 B
Join 160 2 3,104.98 ns 31.0740 ns 3,108.59 ns 0.59 2 856 B
StringBuilder 160 2 885.68 ns 8.3940 ns 885.54 ns 0.17 1 2128 B
Concat 160 5 10,087.94 ns 90.1933 ns 10,107.24 ns 1.00 3 133400 B
Interpolation 160 5 10,315.27 ns 135.8658 ns 10,345.10 ns 1.02 4 133400 B
Join 160 5 4,182.40 ns 40.2419 ns 4,172.52 ns 0.41 2 4368 B
StringBuilder 160 5 1,316.73 ns 18.9785 ns 1,317.23 ns 0.13 1 4184 B
Concat 160 10 17,859.67 ns 269.3302 ns 17,961.00 ns 1.00 4 262352 B
Interpolation 160 10 17,192.77 ns 303.5586 ns 17,215.78 ns 0.96 3 262352 B
Join 160 10 4,195.33 ns 29.1457 ns 4,190.85 ns 0.24 2 8088 B
StringBuilder 160 10 1,544.68 ns 19.0539 ns 1,546.45 ns 0.09 1 7904 B
Concat 160 20 31,768.48 ns 388.4661 ns 31,635.60 ns 1.00 3 520248 B
Interpolation 160 20 31,142.37 ns 826.1903 ns 31,293.68 ns 0.97 3 520248 B
Join 160 20 5,117.99 ns 126.8319 ns 5,061.77 ns 0.16 2 15456 B
StringBuilder 160 20 2,153.02 ns 47.6523 ns 2,158.06 ns 0.07 1 15272 B

N = Number of append iterations; L = Length of substrings

Conclusions

StringBuilder is efficient at smaller scales than I thought, but it's still not clearly an improvement over concatenation until you're doing between 20 and 40 appends, depending on the size of the strings you're appending. StringBuilder is also basically pointless if the strings you're building are not already created; the work it takes to generate them will most likely wipe out any optimization you can do to assemble the final result.

So, should you use StringBuilder? Yes, sometimes. It depends on what your data is like, and what you're optimizing for. Basically, if you have large strings that you're concatenating only a few times, string concat may be more efficient in all criteria. If you're optimizing for memory, then Join has excellent memory efficiency for small strings, and comparable to StringBuilder for large ones. In any case, I think that 40 concats is a good rule of thumb break point for where you get clear performance benefits from StringBuilder regardless of what your data looks like.

But then that brings me to the actual heart of the matter. What are you doing that needs this kind of performance? Look at that chart again. All of these times are measured in nanoseconds. That's 1/1000 of a microsecond. Which is 1/1000 of a millisecond. Which is 1/1000 of a second. Do you really want to bother over a few billionths of a second? And so my real take-away advice here is to optimize for readability. If performance is a real world issue, then and only then should you optimize for performance.


Cover photo by unsplash-logochuttersnap