12

Why is StringBuilder slower when compared to + concatenation? StringBuilder was meant to avoid extra object creation, but why does it penalize performance?

    static void Main(string[] args)
    {
        int max = 1000000;
        for (int times = 0; times < 5; times++)
        {
            Console.WriteLine("\ntime: {0}", (times+1).ToString());
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < max; i++)
            {
                string msg = "Your total is ";
                msg += "$500 ";
                msg += DateTime.Now;
            }
            sw.Stop();
            Console.WriteLine("String +\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));

            sw = Stopwatch.StartNew();
            for (int j = 0; j < max; j++)
            {
                StringBuilder msg = new StringBuilder();
                msg.Append("Your total is ");
                msg.Append("$500 ");
                msg.Append(DateTime.Now);
            }
            sw.Stop();
            Console.WriteLine("StringBuilder\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
        }
        Console.Read();
    }

enter image description here

EDIT: Moving out of scope variables as suggested:

enter image description here

Junior Mayhé
  • 16,144
  • 26
  • 115
  • 161
  • 5
    I think StringBuilder is faster for "bigger" strings. – LarsTech Nov 11 '11 at 00:40
  • adding string.Format and string.Concat, we also notice `string.Concat` is faster – Junior Mayhé Nov 11 '11 at 00:43
  • It's just about always faster unless you're doing a simple single concat and not worth the extra lines of code. As NullUser pointed out, you're allocating inside the loop which is wrong and it skews your numbers. – bryanmac Nov 11 '11 at 00:45
  • 1
    Why is it that everyone automatically assumes string concatenation is "slow"? Repeated string concatenation has a "worse Big-O", but don't forget the `C` and keep the `n` in mind :) Use the correct tool for the job -- it is a rare day when I pull out StringBuilder. (Also, I do not know what optimizations C# does, but Java *may* transform `+` into the equivalent StringBuilder code during compilation.) –  Nov 11 '11 at 00:48
  • 5
    *StringBuilder was meant to avoid extra object creation* - Except you're creating a new `StringBuilder` each iteration for some unknown reason... – Ed S. Nov 11 '11 at 00:51
  • @pst: string concatenation *is* slow! I have a test program that times appending 10,000 characters to a string, and appending 10,000 characters to a `StringBuilder`. Appending to a string takes about 300 times as long. – Jim Mischel Nov 11 '11 at 00:54
  • The difference is alot less if you specify a sufficient capacity when you create the StringBuilder. – MerickOWA Nov 11 '11 at 00:54
  • @pst: +1 for that; in C++ one of the most common performance pitfalls is writing text through a (new) string stream, instead of just push_back-ing into a std::string or std::vector. The irony is that, frequently, they're only doing _more copying_ and also incurring localization overheads. Of course, StringBuilders are much simpler, but the same popular fallacy can be seen at play. – sehe Nov 11 '11 at 00:57
  • @MerickOWA: you did _not_ test that. **Edit** I just did, `EnsureCapacity(60)` makes no difference, `EnsureCapacity(512)` slows it down by 25%, `EnsureCapacity(512)` slows it down by 105% (!!) – sehe Nov 11 '11 at 00:57
  • @pst: You can crack out ILSpy if you want to find out, but my money is on C# *not* doing such things under the covers. That would be a hidden mechanism on a low-level optimization, which might defeat the original purpose of the optimization to begin with because of the allocation of the `StringBuilder` object. – Merlyn Morgan-Graham Nov 11 '11 at 01:03
  • @sehe I did test it, calling "new StringBuilder(64)" was nearly the same as string concatenation. String +: 2959ms, StringBuilder: 3122ms, StringBuilder(64): 2902ms – MerickOWA Nov 11 '11 at 01:24
  • @MerickOWA Ok, then: on my box StringBuilder(64) made no difference. Thanks for sharing. – sehe Nov 11 '11 at 08:25

7 Answers7

16

Change so that the StringBuilder isn't instantiated all the time, instead .Clear() it:

time: 1
String +    :   3348ms
StringBuilder   :   3151ms

time: 2
String +    :   3346ms
StringBuilder   :   3050ms

etc.

Note that this still tests exactly the same functionality, but tries to reuse resources a bit smarter.

Code: (also live on http://ideone.com/YuaqY)

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

public class Program
{
    static void Main(string[] args)
    {
        int max = 1000000;
        for (int times = 0; times < 5; times++)
        {
            {
                Console.WriteLine("\ntime: {0}", (times+1).ToString());
                Stopwatch sw = Stopwatch.StartNew();
                for (int i = 0; i < max; i++)
                {
                    string msg = "Your total is ";
                    msg += "$500 ";
                    msg += DateTime.Now;
                }
                sw.Stop();
                Console.WriteLine("String +\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
            }

            {
                Stopwatch sw = Stopwatch.StartNew();
                StringBuilder msg = new StringBuilder();
                for (int j = 0; j < max; j++)
                {
                    msg.Clear();
                    msg.Append("Your total is ");
                    msg.Append("$500 ");
                    msg.Append(DateTime.Now);
                }
                sw.Stop();
                Console.WriteLine("StringBuilder\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
            }
        }
        Console.Read();
    }
}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • indeed I was missing the `.Clear()`. The cost droped a lot for StringBuilder. Now it is 3196ms! Sometimes it gets slower but it has improved considering this adjust. :D – Junior Mayhé Nov 11 '11 at 00:53
  • @JuniorMayhé A difference of a couple hundred milliseconds (out of 3000+) is not significant. You get orders of magnitude in difference between using SB and `+` when dealing with large strings: http://ideone.com/SNBr1 – NullUserException Nov 11 '11 at 01:01
  • @NullUserExceptionఠ_ఠ: we all know that. I was curious whether I could make it quicker without changing the test. Turns out, yes you can. Of course this is a bad example of synthesized test, but it neatly refutes the point that 'StringBuilder' would be slower: it's not necessarily, even if used for non-optimal tasks – sehe Nov 11 '11 at 01:05
  • 1
    @sehe Indeed, that surprised me a bit too. +1 for showing it ;) – NullUserException Nov 11 '11 at 01:07
  • Your ideone link gives me a compilation error though. PS: loved you how just created a new scope instead of renaming the variable... LOL – NullUserException Nov 11 '11 at 01:08
  • @NullUserExceptionఠ_ఠ: good point. **[StringBuilder.Clear is new in .NET #4.0](http://msdn.microsoft.com/en-us/library/system.text.stringbuilder.clear.aspx)** and ideone is on 3.5. Now emulating with `msg.Remove(0, msg.Length)` - same performance gain – sehe Nov 11 '11 at 01:16
8

You are creating a new instance of StringBuilder with every iteration, and that incurs some overhead. Since you are not using it for what it's actually meant to do (ie: build large strings which would otherwise require many string concatenation operations), it's not surprising to see worse performance than concatenation.

A more common comparison / usage of StringBuilder is something like:

string msg = "";
for (int i = 0; i < max; i++)
{
    msg += "Your total is ";
    msg += "$500 ";
    msg += DateTime.Now;
}

StringBuilder msg_sb = new StringBuilder();
for (int j = 0; j < max; j++)
{
    msg_sb.Append("Your total is ");
    msg_sb.Append("$500 ");
    msg_sb.Append(DateTime.Now);
}

With this, you'll observe a significant performance difference between StringBuilder and concatenation. And by "significant" I mean orders of magnitude, not the ~ 10% difference you are observing in your examples.

Since StringBuilder doesn't have to build tons of intermediary strings that will just get thrown away, you get much better performance. That's what it's meant for. For smaller strings, you are better off using string concatenation for simplicity and clarity.

NullUserException
  • 83,810
  • 28
  • 209
  • 234
  • 2
    you're measuring something entirely different too, though – sehe Nov 11 '11 at 00:45
  • 1
    +1 - as NullUser pointed out you only need to allocate once and then append inside the loop – bryanmac Nov 11 '11 at 00:46
  • 2
    @JuniorMayhé This is the correct answer. Having the StringBuilder and concatenation in the same loop like in your example doesn't tell the whole story since the StringBuilder has to wait for the concatenation to occur. The larger your string becomes, the faster StringBuilder becomes too. – Pete Nov 11 '11 at 00:49
  • @sehe I know; I amended my answer accordingly. – NullUserException Nov 11 '11 at 00:50
2

The benefits of StringBuilder should be noticeable with longer strings.

Every time you concatenate a string you create a new string object, so the longer the string, the more is needed to copy from the old string to the new string.

Also, creating many temporary objects may have an adverse effect on performance that is not measurable by a StopWatch, because it "pollutes" the managed heap with temporary objects and may cause more garbage collection cycles.

Modify your test to create (much) longer strings and use (many) more concatenations / append operations and the StringBuilder should perform better.

Motti Shaked
  • 1,854
  • 15
  • 8
2

Note that

string msg = "Your total is ";
msg += "$500 ";
msg += DateTime.Now;

compiles down to

string msg = String.Concat("Your total is ", "$500 ");
msg = String.Concat(msg, DateTime.Now.ToString());

This totals two concats and one ToString per iteration. Also, a single String.Concat is really fast, because it knows how large the resulting string will be, so it only allocates the resulting string once, and then quickly copies the source strings into it. This means that in practice

String.Concat(x, y);

will always outperform

StringBuilder builder = new StringBuilder();
builder.Append(x);
builder.Append(y);

because StringBuilder cannot take such shortcuts (you could call a thirs Append, or a Remove, that's not possible with String.Concat).

The way a StringBuilder works is by allocating an initial buffer and set the string length to 0. With each Append, it has to check the buffer, possibly allocate more buffer space (usually copying the old buffer to the new buffer), copy the string and increment the string length of the builder. String.Concat does not need to do all this extra work.

So for simple string concatenations, x + y (i.e., String.Concat) will always outperform StringBuilder.

Now, you'll start to get benefits from StringBuilder once you start concatenating lots of strings into a single buffer, or you're doing lots of manipulations on the buffer, where you'd need to keep creating new strings when not using a StringBuilder. This is because StringBuilder only occasionally allocates new memory, in chunks, but String.Concat, String.SubString, etc. (nearly) always allocate new memory. (Something like "".SubString(0,0) or String.Concat("", "") won't allocate memory, but those are degenerate cases.)

Ruben
  • 15,217
  • 2
  • 35
  • 45
2

In addition to not using StringBuilder as in the most efficient manner, you're also not using string concatenation as efficiently as possible. If you know how many strings you're concatenating ahead of time, then doing it all on one line should be fastest. The compiler optimizes the operation so that no intermediate strings are generated.

I added a couple more test cases. One is basically the same as what sehe suggested, and the other generates the string in one line:

sw = Stopwatch.StartNew();
builder = new StringBuilder();
for (int j = 0; j < max; j++)
{
    builder.Clear();
    builder.Append("Your total is ");
    builder.Append("$500 ");
    builder.Append(DateTime.Now);
}
sw.Stop();
Console.WriteLine("StringBuilder (clearing)\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));

sw = Stopwatch.StartNew();
for (int i = 0; i < max; i++)
{
    msg = "Your total is " + "$500" + DateTime.Now;
}
sw.Stop();
Console.WriteLine("String + (one line)\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));

And here is an example of the output I see on my machine:

time: 1
String +    :   3707ms
StringBuilder   :   3910ms
StringBuilder (clearing)    :   3683ms
String + (one line) :   3645ms

time: 2
String +    :   3703ms
StringBuilder   :   3926ms
StringBuilder (clearing)    :   3666ms
String + (one line) :   3625ms

In general: - StringBuilder does better if you're building a large string in a lot of steps, or you don't know how many strings will be concatenated together.
- Mashing them all together in a single expression is better whenever it's a reasonable option option.

Sean U
  • 6,730
  • 1
  • 24
  • 43
0

I think its better to compare effeciancy between String and StringBuilder rather then time.

what msdn says: A String is called immutable because its value cannot be modified once it has been created. Methods that appear to modify a String actually return a new String containing the modification. If it is necessary to modify the actual contents of a string-like object, use the System.Text.StringBuilder class.

string msg = "Your total is "; // a new string object
msg += "$500 "; // a new string object
msg += DateTime.Now; // a new string object

see which one is better.

Muhammad Saifuddin
  • 1,284
  • 11
  • 29
0

Here is an example that demonstrates a situation in which StringBuilder will execute more quickly than string concatenation:

static void Main(string[] args)
{
    const int sLen = 30, Loops = 10000;
    DateTime sTime, eTime;
    int i;
    string sSource = new String('X', sLen);
    string sDest = "";
    // 
    // Time StringBuilder.
    // 
    for (int times = 0; times < 5; times++)
    {
        sTime = DateTime.Now;
        System.Text.StringBuilder sb = new System.Text.StringBuilder((int)(sLen * Loops * 1.1));
        Console.WriteLine("Result # " + (times + 1).ToString());
        for (i = 0; i < Loops; i++)
        {
            sb.Append(sSource);
        }
        sDest = sb.ToString();
        eTime = DateTime.Now;
        Console.WriteLine("String Builder took :" + (eTime - sTime).TotalSeconds + " seconds.");
        // 
        // Time string concatenation.
        // 
        sTime = DateTime.Now;
        for (i = 0; i < Loops; i++)
        {
            sDest += sSource;
            //Console.WriteLine(i);
        }
        eTime = DateTime.Now;
        Console.WriteLine("Concatenation took : " + (eTime - sTime).TotalSeconds + " seconds.");
        Console.WriteLine("\n");
    }
    // 
    // Make the console window stay open
    // so that you can see the results when running from the IDE.
    // 
}

Result # 1 String Builder took :0 seconds. Concatenation took : 8.7659616 seconds.

Result # 2 String Builder took :0 seconds. Concatenation took : 8.7659616 seconds.

Result # 3 String Builder took :0 seconds. Concatenation took : 8.9378432 seconds.

Result # 4 String Builder took :0 seconds. Concatenation took : 8.7972128 seconds.

Result # 5 String Builder took :0 seconds. Concatenation took : 8.8753408 seconds.

StringBulder is much faster than + concatenation..

Servy
  • 202,030
  • 26
  • 332
  • 449