Search code examples
c#performancestringbuilder

Why StringBuilder.Replace is slower than String.Replace


According to the following unit test methods, StringBuilder is far slower than String.Replace, how come every one saying StringBuilder is faster? Am I missing something?

[TestMethod]
public void StringReplace()
{
    DateTime date = DateTime.Now;
    string template = File.ReadAllText("file.txt");
    for (int i = 0; i < 100000; i++)
    {
        template = template.Replace("cat", "book" );
        template = template.Replace("book", "cat"); 
    }
    Assert.Fail((DateTime.Now - date).Milliseconds.ToString()); 
}

[TestMethod]
public void StringBuilder()
{
    DateTime date = DateTime.Now;
    StringBuilder template = new StringBuilder(File.ReadAllText("file.txt"));
    for (int i = 0; i < 100000; i++)
    {
        template.Replace("cat", "book");
        template.Replace("book", "cat"); 
    }
    Assert.Fail((DateTime.Now - date).Milliseconds.ToString());
}

Here is the result:

StringReplace - 335ms

StringBuilder - 799ms


Solution

  • According to several tests (links to more tests at the bottom) as well as a quick and sloppy test of my own, String.Replace performs better than StringBuilder.Replace. You do not seem to be missing anything.

    For completeness sake, here's my testing code:

    int big = 500;
    String s;
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 100; ++i)
    {
        sb.Append("cat mouse");
    }
    s = sb.ToString();
    
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < big; ++i)
    { 
        s = s.Replace("cat", "moo"); 
        s = s.Replace("moo", "cat"); 
    }
    sw.Stop(); Trace.WriteLine(sw.ElapsedMilliseconds); sw.Reset(); sw.Start();
    for (int i = 0; i < big; ++i)
    {
        sb.Replace("cat", "moo");
        sb.Replace("moo", "cat");
    }
    sw.Stop(); Trace.WriteLine(sw.ElapsedMilliseconds); sw.Reset(); sw.Start();
    for (int i = 0; i < big; ++i)
    {
        s = s.Replace("cat", "mooo");
        s = s.Replace("mooo", "cat");
    }
    sw.Stop(); Trace.WriteLine(sw.ElapsedMilliseconds); sw.Reset(); sw.Start();
    for (int i = 0; i < big; ++i)
    {
        sb.Replace("cat", "mooo");
        sb.Replace("mooo", "cat");
    }
    sw.Stop(); Trace.WriteLine(sw.ElapsedMilliseconds);
    

    The output, on my machine, is:

    9
    11
    7
    1977
    

    [EDIT]

    I missed one very important case. That is the case where every time the string is replaced with something else. This could matter because of the way C# handles strings. What follows is the code that tests the missing case, and the results on my system.

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;
    class Program
    {
        static void Main()
        {
            var repl = GenerateRandomStrings(4, 500);
            String s;
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 100; ++i)
            {
                sb.Append("cat mouse");
            }
            s = sb.ToString();
            Stopwatch sw = new Stopwatch();
            sw.Start();
            foreach (string str in repl)
            {
                s = s.Replace("cat", str);
                s = s.Replace(str, "cat");
            }
            sw.Stop(); Trace.WriteLine(sw.ElapsedMilliseconds); sw.Reset(); sw.Start();
            foreach (string str in repl)
            {
                sb.Replace("cat", str);
                sb.Replace(str, "cat");
            }
            sw.Stop(); Trace.WriteLine(sw.ElapsedMilliseconds);
        }
    
        static HashSet<string> GenerateRandomStrings(int length, int amount)
        {
            HashSet<string> strings = new HashSet<string>();
            while (strings.Count < amount)
                strings.Add(RandomString(length));           
            return strings;
        }
    
        static Random rnd = new Random();
        static string RandomString(int length)
        {
            StringBuilder b = new StringBuilder();
            for (int i = 0; i < length; ++i)
                b.Append(Convert.ToChar(rnd.Next(97, 122)));
            return b.ToString();
        }
    }
    

    Output:

    8
    1933
    

    However, as we start to increase the length of the random strings, the StringBuilder solution comes closer and closer to the String solution. For random strings with a length of 1000 characters, my results are

    138
    328
    

    Using this new knowledge on the old tests, I get similar results when increasing the length of the string to replace with. When replacing with a string that is a thousand 'a' characters instead of "mooo", my results for the original answer become:

    8
    11
    160
    326
    

    Although the results do become closer, it still seems that for any real world use, String.Replace beats StringBuilder.Replace.