TheDeveloperBlog.com


C# StringBuilder Cache

StringBuilder cache. StringBuilder is an object. It is used as an optimization. In many programs the StringBuilder itself is created many times. With a StringBuilder cache you can reduce this cost further.


Example. We present an example program with two methods. The first version, Method1, creates a new StringBuilder in every call. It then adds strings to the StringBuilder and returns the string representation.

StringBuilder ToString

And: The second version, Method2, references a static StringBuilder field and then clears it each time.

Note: The StringBuilder field will remain allocated in one part of memory throughout all calls to Method2.

Based on:

.NET 4.5

Method that creates new StringBuilder: C#

static string Method1()
{
    StringBuilder builder = new StringBuilder();
    foreach (string value in _array)
	builder.Append(value);
    return builder.ToString();
}

Method that uses cached StringBuilder: C#

static StringBuilder _builder = new StringBuilder();

static string Method2()
{
    StringBuilder builder = _builder;
    builder.Clear();
    foreach (string value in _array)
	builder.Append(value);
    return builder.ToString();
}


Benchmark. Does storing the StringBuilder in a field cache improve performance here? The main advantage we see with the field is that the single StringBuilder has its capacity changed fewer times.

Also: Fewer allocations occur for the StringBuilder itself, not just its buffer.

C# program that tests StringBuilder cache

using System;
using System.Diagnostics;
using System.Text;

class Program
{
    static string Method1()
    {
	StringBuilder builder = new StringBuilder();
	foreach (string value in _array)
	    builder.Append(value);
	return builder.ToString();
    }

    static StringBuilder _builder = new StringBuilder();

    static string Method2()
    {
	StringBuilder builder = _builder;
	builder.Clear();
	foreach (string value in _array)
	    builder.Append(value);
	return builder.ToString();
    }

    static string[] _array = { "dot", "net", "perls", "string", "array" };
    const int _max = 1000000;
    static void Main()
    {
	Method1();
	Method2();

	// Version 1: use new StringBuilder each time.
	var s1 = Stopwatch.StartNew();
	for (int i = 0; i < _max; i++)
	{
	    Method1();
	}
	s1.Stop();

	// Version 2: reuse a single StringBuilder instance.
	var s2 = Stopwatch.StartNew();
	for (int i = 0; i < _max; i++)
	{
	    Method2();
	}
	s2.Stop();
	Console.WriteLine(s1.Elapsed.TotalMilliseconds);
	Console.WriteLine(s2.Elapsed.TotalMilliseconds);
	Console.Read();
    }
}

Results

140.0784   Cached StringBuilder (Method1)
 84.4145   New StringBuilder    (Method2)

Here, if you adjust the capacity for the StringBuilder, Method1 will perform faster than before but still slower than Method2. This is because Method2 does not require capacity adjustments after the first call in the benchmark.

StringBuilder Capacity

Warning: This can reduce performance. A slowdown occurs when the buffers are large and clearing them is slower than allocating new ones.

And: Relying on the garbage collector may in this case be faster than using this kind of cache.

Note: Thanks to Alex Lupu for helping improve the benchmark on this page. The previous version was confusing.


Summary. Using the StringBuilder type as a field can reduce memory allocations and improve performance. Unfortunately, you do need to call Clear every time it is accessed or the results will be incorrect.

StringBuilder Clear