FeedC# TutorialArchiveAbout

C# String.Format() and StringBuilder

C# String.Format() and StringBuilder

Note: If you are not familiar with String.Format and StringBuilder you can learn about it in my blog post C# String.

Recently, I saw some code that looked something like this:

StringBuilder builder = new StringBuilder();
builder.Append(String.Format("{0} {1}", firstName, lastName));
// Do more with builder...

Now, I don’t wana get into arguments about how String.Concat() is more performant here. String.Format() allows code to be more easily localized and it is being used for that purpose here. The real problem is that StringBuilder.AppendFormat() should be used instead:

StringBuilder builder = new StringBuilder();
builder.AppendFormat("{0} {1}", firstName, lastName);
// Do more with builder...

The reason that this is important is because, internally, String.Format() actually creates a StringBuilder and calls StringBuilder.AppendFormat()! String.Format() is implemented something like this:

public static string Format(IFormatProvider provider, string format, params object[] args)
{
  if (format == null || args == null)
   throw new ArgumentNullException((format == null ? "format" : "args"));

  StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
  builder.AppendFormat(provider, format, args);
  return builder.ToString();
}

you can see the actual implementation in the source code for the runtime of .NET Core. Here is the link for the same: String.Manipulation.cs

In String.Manipulation.cs file you will find the Format method:

From the above code, you can see that Format method calls an internal method FormatHelper.

Now, the FormatHelper method uses StringBuilder for formatting the text using StringBuilderCache.

It turns out that the formatting logic is actually implemented in StringBuilder.AppendFormat(). So, the original code actually caused a second StringBuilder to be utilized when it wasn’t needed.

This is also important to know if you are trying to avoid creating a StringBuilder by concatentating strings with String.Format(). For example:

string nameString = "<td>" + String.Format("{0} {1}", firstName, lastName) + "</td>"
  + "<td>" + String.Format("{0}, {1}", id, department) + "</td>";

That code will actually create two StringBuilders, if the size of formatted string is greater than MaxBuilderSize, used in StringBuilderCache, which is set to 360. So, creating one StringBuilder and using AppendFormat() will be more performent:

StringBuilder nameBuilder = new StringBuilder();
nameBuilder.Append("<td>");
nameBuilder.AppendFormat("{0} {1}", firstName, lastName);
nameBuilder.Append("</td>");
nameBuilder.Append("<td>");
nameBuilder.AppendFormat("{0}, {1}", id, department);
nameBuilder.Append("</td>");
string nameString = nameBuilder.ToString();

I decided to run some performance tests to verify my claims. First, I timed code that demonstrates the very reason that StringBuilder exists:

const int LOOP_SIZE = 10000;
const string firstName = "Shadman";
const string lastName = "Kudchikar";
const int id = 1;
const string department = ".NET Team";

static void PerformanceTest1()
{
  string nameString = String.Empty;

  for (int i = 0; i < LOOP_SIZE; i++)
    nameString += String.Format("{0} {1}", firstName, lastName);
}

The above code creates a new string and then concatenates to it inside of a for-loop. This causes two new strings to be created on each pass–one from String.Format() and one from the concatenation. This is woefully inefficient.

Next, I tested the same code modified to use a StringBuilder with String.Format():

static void PerformanceTest2()
{
  StringBuilder builder = new StringBuilder((firstName.Length + lastName.Length + 1) * LOOP_SIZE);

  for (int i = 0; i < LOOP_SIZE; i++)
    builder.Append(String.Format("{0} {1}", firstName, lastName));

  string nameString = builder.ToString();
}

Finally, I tested code that uses StringBuilder.AppendFormat() instead of String.Format():

static void PerformanceTest3()
{
  StringBuilder builder = new StringBuilder((firstName.Length + lastName.Length + 1) * LOOP_SIZE);

  for (int i = 0; i < LOOP_SIZE; i++)
    builder.AppendFormat("{0} {1}", firstName, lastName);

  string nameString = builder.ToString();
}

These three methods ran with the following timings:

For .NET Framework:

PerformanceTest1: 0.21045 seconds
PerformanceTest2: 0.001585 seconds
PerformanceTest3: 0.0010846 seconds

For .NET Core:

PerformanceTest1: 0.173821 seconds
PerformanceTest2: 0.0012753 seconds
PerformanceTest3: 0.0007812 seconds

Obviously, concatenating a string in a loop without using a StringBuilder is amazingly inefficient. And, removing the call to String.Format also yields a performance boost.

Next, I tested the following two methods:

static void PerformanceTest4()
{
  string htmlString;
  for (int i = 0; i < LOOP_SIZE; i++)
    htmlString = "<td>" + String.Format("{0} {1}", firstName, lastName) + "</td><td>"
      + String.Format("{0}, {1}", id, department) + "</td>";
}
static void PerformanceTest5()
{
  StringBuilder builder = new StringBuilder(256);

  string htmlString;
  for (int i = 0; i < LOOP_SIZE; i++)
  {
    builder.Append("<td>");
    builder.AppendFormat("{0} {1}", firstName, lastName);
    builder.Append("</td><td>");
    builder.AppendFormat("{0}, {1}", id, department);
    builder.Append("</td>");
    htmlString = builder.ToString();
    builder.Length = 0;
  }
}

These two methods ran with the following timings:

For .NET Framework:

PerformanceTest4: 0.0050095 seconds
PerformanceTest5: 0.0044467 seconds

For .NET Core:

PerformanceTest4: 0.0036363999999999997 seconds
PerformanceTest5: 0.0019971 seconds

As you can see, it is important to know when to use String.Format and when to use StringBuilder.AppendFormat(). While the performance boosts that can be achieved are fairly small, they are too easy to code.

You can download the performance tests here: https://github.com/kudchikarsk/string-format-performance-test.

Further Reading


comments powered by Disqus