String.Format() vs. Concatenation vs. String Builder
What’s the fastest way to build a string
in C#? We have several options available to us. string.Format()
is a function built right in to the string
class., Concatenation ("a" + "b"
) is a feature of the language itself! The System.Text.StringBuilder
class is a built in class with a name that makes it sound like it’s purpose-built for building strings. Today I pit these three against each other to find out just which one you should be using to build strings as quickly as possible.
First there is the string.Format()
function. Like printf() in C and C++, it takes a “format string” which optionally specifies placeholders that are filled in with the rest of the parameters to the function. Here are some examples:
// Placeholders are of the form: {index} string.Format("{0} + {1} = {2}", 2, 3, 5); // returns "2 + 3 = 5" // Mixed types are OK, too string.Format("{0} {1}, {2}", "August", 5, 1972); // returns "August 5, 1972"
Concatenation is dead simple since it’s built into the language:
// Strings can be concatenated by the + operator "Hello," + " " + "world!"; // evaluates to: "Hello, world!" // Mixed types are OK, too "August " + 5 + ", " + 1972; // evaluates to: "August 5, 1972"
Finally, the System.Text.StringBuilder
class allows you to incrementally build a string via its many overloaded Append
functions:
var builder = new StringBuilder(); // initially empty var builder = new StringBuilder(1024); // empty, but with 1024 bytes of capacity var builder = new StringBuilder("hello"); // initially "hello" // Append various types builder.Append("string"); builder.Append('c'); builder.Append(123); builder.Append(3.1415); // Get the result string var str = builder.ToString();
Now let’s pit these string-building approaches against each other to find out which is fastest and which is slowest. Here’s a C# script for Unity that builds a string out of 1000 single-character strings with each of the above approaches:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using UnityEngine; public static class StopwatchExtensions { public static long RunTests( this Stopwatch stopwatch, int numIterations, Action testFunction ) { stopwatch.Reset(); stopwatch.Start(); for (var i = 0; i < numIterations; ++i) { testFunction(); } return stopwatch.ElapsedMilliseconds; } } public class TestScript : MonoBehaviour { private string report; void Start() { var numIterations = 10000; var formatString = ""; for (var i = 0; i < 1000; ++i) { formatString += "{" + i + "}"; } var stopwatch = new Stopwatch(); var stringFormatTime = stopwatch.RunTests( numIterations, () => StringFormat(formatString) ); var concatenationTime = stopwatch.RunTests( numIterations, () => Concatenation() ); var stringBuilderTime = stopwatch.RunTests( numIterations, () => StringBuilder() ); report = "Test,Time\n" + "String.Format," + stringFormatTime + "\n" + "Concatenation," + concatenationTime + "\n" + "String Builder," + stringBuilderTime; } void OnGUI() { GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report); } private string StringFormat(string formatString) { var s = "A"; return string.Format( formatString, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s ); } private string Concatenation() { var s = "A"; return s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s; } private string StringBuilder() { var s = "A"; var builder = new StringBuilder(); for (int i = 0; i < 1000; ++i) { builder.Append(s); } return builder.ToString(); } }
If you want to try out the test yourself, simply paste the above code into a TestScript.cs
file in your Unity project’s Assets
directory and attach it to the main camera game object in a new, empty project. Then build in non-development mode for 64-bit processors and run it windowed at 640×480 with fastest graphics. I ran it that way on this machine:
- 2.3 Ghz Intel Core i7-3615QM
- Mac OS X 10.10.2
- Unity 5.0.0f4, Mac OS X Standalone, x86_64, non-development
- 640×480, Fastest, Windowed
And got these results:
Test | Time |
---|---|
String.Format | 3669 |
Concatenation | 1108 |
StringBuilder | 1007 |
string.Format()
is the clear loser in this test. It’s taking over 3x longer to build the same string using that method than the others.
Next is good old concatenation. It’s nearly tied for first place, but a bit behind the winner: StringBuilder
. There’s not much between them though as even this extreme of a test only shows a 10% performance difference.
The bottom line here is that serious string building should be done via StringBuilder
. If you find it cumbersome to use and prefer to read and write using one of the other methods, it probably won’t matter to performance if you’re not building many strings or large strings. That’s especially true if your preferred method is concatenation where only extreme cases (e.g. a JSON serializer library) will show much of a performance difference when upgrading to StringBuilder
.
There are some potential further optimizations to be had with StringBuilder
on top of what we’ve already seen. In the above test the StringBuilder
instance started out empty. Given an initial capacity large enough to hold the whole string would have boosted its performance even further. That does require you to know how much space you’re going to need ahead of time though, or at least be able to approximate.
Got a preference for one of the above methods of string building? Post a comment letting me know which one and why!
#1 by dknox on April 10th, 2015 ·
Are there any particular strategies/examples for ensuring there are no allocations for strings in something like a score/points ticker whose values update often and likely in some incremental form?
#2 by jackson on April 10th, 2015 ·
For better or worse, strings in C# are immutable objects just as they are in Java, JavaScript, and AS3. Once you’ve built one—
"Score: 2000"
—you can’t ever change it. Instead, you have to allocate a newString
object with different contents. So if you want to work around any allocations, you’ll have to stop using aString
class for changing values.There are alternatives, though. You could use a
char[]
and render that. You’re allowed to change arrays, so updating it is possible. You won’t get access to all the niceString
-oriented functions like regular expressions and theString
public API itself, but basic operations like concatenation shouldn’t be too hard to write yourself. Of course you’ll end up with achar[]
, so your rendering code will need to be able to render from that instead of aString
.Before going through all this trouble, I’d first start by profiling the effect of creating a
String
or two each time you update the score. It’s likely to not have enough of an impact that it’d be worth your time to optimize it. That’s just a guess, though. A profiler will give you the data you need to make the decision.#3 by afrokick on May 22nd, 2015 ·
update your label’s text only after changes.
#4 by jackson on May 22nd, 2015 ·
Good point- sometimes your string is only the result of some other data changing. If the underlying data (points in this case) hasn’t changed, just keep using the cached string. That avoids building the string at all.
#5 by Creatify on April 10th, 2015 ·
Another great article, thank you! Have you thought of doing an XML use comparison of AS3 vs. C#? I’m getting into that now, and I’m missing the good ol’ E4X approach.
#6 by jackson on April 10th, 2015 ·
Do you mean a comparison of how XML is used on each platform or a performance comparison?
#7 by Creatify on April 14th, 2015 ·
Yes, a comparison of how they’re used on each platform. I’m curious if there are C# techniques that ‘mirror’ techniques within E4X, I’m just starting to research this.
#8 by Shawn Blais Skinner on May 19th, 2015 ·
Thanks Jackson. What I was really hoping to see, was some analysis of the garbage created with all 3 of these techniques. Maybe in a future article?
#9 by jackson on May 19th, 2015 ·
Sounds like a good idea for an article. I’ve added it to my list so you may see it soon!
#10 by Scully on December 8th, 2015 ·
This is a bit misleading because your example uses values that are known at compile time, which can be optimised by the compiler. Once the values are not known until runtime, I believe concatenation will result in a lot of intermediate strings that will then need to be garbage collected. Running the following test, I’m finding string.Format faster:
#11 by jackson on December 8th, 2015 ·
Using a constant versus a literal could definitely matter. I notice that you tested using
Console.WriteLine
which doesn’t print anything by default (see my workaround) in Unity. Which environment did you test in? .NET? Mono? Or was this tested in Unity?I changed the
Console.WriteLine
calls to save to a string then display that inOnGUI
, like in the article. Then ran it in Unity 5.2.3f1 with the same machine from the article and got these results:So concatenation is faster using your test, too. I suspect that it’s a difference in .NET runtime, such as not running the test within Unity. Let me know where you ran it as I’d be interested to know where the difference is.
#12 by Scully on December 9th, 2015 ·
Hello Jackson, the test was done using a simple console application in MS .Net Framework version 4.
My results are:
00:00:00.8386656
00:00:00.7140645
However, if you swap the order of test round (ie string.Format first), the one that goes first always seems to be slower. Even prepping the runtime with a disregarded initial test run loop… If you run them separately (ie only one loop included per run of the test app), the results are inconsistent.
I think the main reason why string.Format(…) is usually recommended as best practice is due to memory, though – there won’t be lots of intermediate results thrown away as there will be with standard string concatenation (ie of 3 or more strings). This could be an issue when working with larger strings or performing a large number of concatenation with smaller strings (in loops and such) – when garbage collection kicks in, it will need to clean up all of these resources.
#13 by jackson on December 9th, 2015 ·
Hey Scully. That’s an interesting theory about swapping the test ordering around. Given the complexity of memory allocation with C#/.NET/Mono I could see that being a penalty to one of the tests. So I swapped around the tests and re-ran with the same environment as my last comment. Here’s what I got:
It’s basically the same numbers, but in reverse order. So string concatenation is still faster in Unity, regardless of the ordering. The key, I think, is the “in Unity” part. There are bound to be lots of performance differences between Unity’s old, forked Mono implementation of .NET compared to Microsoft’s implementation. Perhaps Microsoft has optimized
string.Format
better than Mono has or Mono has optimized string concatenation better than Microsoft. I don’t really know what the cause is, but the results are consistently in favor of string concatenation in Unity.You also make a good point about garbage creation. That’s a really important topic with Unity since its Mono has a very slow GC implementation that happens all on one frame. Several of my most recent articles have focused on garbage creation. So I adapted your code for a quick test using the Unity profiler. Here are the two test functions:
I called them from the
Start
function on myMonoBehaviour
and looked at the Unity profiler (in “deep” mode). The results were thatstring.Format
allocated 472 bytes of garbage while string concatenation allocated 324 bytes. String concatenation therefore creates less garbage, at least in Unity’s Mono implementation. The detailed results are quite complicated, so I took screenshots and posted them: string.Format, string concatenation.#14 by Scully on December 10th, 2015 ·
Hello Jackson, I have profiled the memory with dotMemory using the console application and get no difference between the two in terms of memory allocation. That really surprises me! There must be some optimisation in the .Net runtime that doesn’t create the intermediate strings, which makes sense I guess, just contrary to my understanding of how it worked!
#15 by jackson on December 10th, 2015 ·
Hey Scully, thanks for reporting back with your results. I would have expected the intermediate strings to be created too, but I suppose this is a common enough use case that there’s optimization going on behind the scenes.
#16 by Amin.mp on June 19th, 2019 ·
Hi,
I suggest:
1. Create random strings.
2. Run the test for a longer period.
Then compare the results and memory also.