Saturday, August 1, 2020

Is StringBuilder faster than appending to a string?

Yes - eventually. We all know most languages such as Java and C# have immutable strings. That is, once created, they cannot be modified. A line of code such as...


html += "size=\"200\"";


...actually creates a new variable called html and allows the old value to be garbage collected. So the old value of html has to be copied to a new location so that it can be appended to. The question in my mind is, how big does the string have to be, for this to start becoming slower than StringBuilder (or StringBuffer in Java)? The answer surprised me.

I'm reading Robert Morris' excellent book "Clean Code" on my Kindle and at location 7955 there is a code snippet.


I wondered if it really made sense to use a StringBuffer here. Surely the cost of the StringBuffer will exceed the cost of immutable strings. Here's some code that tries to answer that question. It runs two versions of this code each with different numbers of appends to see where the break even point is. Obviously it is a console project in C#.

using System;
using System.Text;

namespace StringBufferTest
{
    class Program
    {
        static void Main(string[] args)
        {
            int Iterations = 1000;

            Console.WriteLine(string.Format("All timings are for {0} iterations,", Iterations));
            GetExecutionTimeSpan(RenderWithString, 1, 1);   // Throw away call because first call incurs overhead that we don't want reflected in our results
            GetExecutionTimeSpan(RenderWithStringBuilder, 1,1);

            for (int AttributeCount = 1; AttributeCount < 20; AttributeCount++)
            {
                Console.WriteLine(string.Format("String        time for {0} appends is {1}ms", AttributeCount, GetExecutionTimeSpan(RenderWithString, Iterations, AttributeCount).TotalMilliseconds));
                Console.WriteLine(string.Format("StringBuilder time for {0} appends is {1}ms", AttributeCount, GetExecutionTimeSpan(RenderWithStringBuilder, Iterations, AttributeCount).TotalMilliseconds));
                Console.WriteLine("-----------------------------");
            }
        }

        static TimeSpan GetExecutionTimeSpan(Func<int, string> f, int Iterations, int AttributeCount)
        {
            DateTime Start = DateTime.Now;
            for (int i = 0; i < Iterations; i++)
                f(AttributeCount);
            return DateTime.Now - Start;
        }

        static string RenderWithString(int AttributeCount)
        {
            string s = "<html";
            for (int i = 0; i < AttributeCount; i++)
                s += " size=\"" + (AttributeCount + 1) + "\"";
            s += ">";
            return s;
        }

        static string RenderWithStringBuilder(int AttributeCount)
        {
            StringBuilder sb = new StringBuilder("<html");
            for (int i = 0; i < AttributeCount; i++)
                sb.Append(" size=\"").Append(AttributeCount + 1).Append("\"");
            sb.Append(">");
            return sb.ToString();
        }
    }
}
    
The timings will vary slightly from run to run, but I find the break even point is around 6 appends. Clearly, always using StringBuilder when appending strings is not always the best strategy.



No comments:

Post a Comment