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.
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 chuttersnap