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 Building Performance Graph

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!