One of the performance improvements we made in .Net was with System.Text.StringBuilder. StringBuilder is used in lots of code to build strings: it has various methods to modify strings quickly. Once a string is built, the ToString method is called to create the string.

We observed via MemSpect that there were thousands of instances of empty strings being created in various scenarios, such as when VS is starting, loading or building a solution. An empty string? Why would that matter?

Warning: an Empty String (“”) is very different from NULL (nothing in VB).

StringBuilder is used in many scenarios that produce empty strings. For example, reading and writing XML, perhaps for serializing and deserializing. An XML class has a member for XML Attributes, which was represented by a StringBuilder to build the attributes. Thousands of XML nodes had empty attributes, and thus created these empty strings.

It turns out that an empty string takes up space: 14 bytes in a 32 bit managed application. It’s an instance of a managed class, System.String. The memory layout of all CLR classes includes the ClassId (unique for each class) as the first 4 bytes and 4 bytes overhead. System.String adds 4 bytes for the length. The CLR Garbage collector has to deal with thousands of these in the managed heap, track the references to them, move them around when compacting, etc.

The fix is to use the globally available static System.String.Empty Field , which will result in the same logical behavior, but not create new empty strings, reducing memory pressure and increasing performance. So the StringBuilder.ToString method was modified to return String.Empty for zero length strings.

In general, it’s better to use String.Empty than “” in source code because the latter will create a new empty string that may be coalesced by the compiler within the module bounds, but not across assemblies. You want to avoid having dozens of modules, each having its own “”

To examine the effect of thousands of empty strings produced by StringBuilder, the code below defines a Measurement action that takes 2 parameters: a description and a lambda expression that will be invoked. The action is invoked twice: once for StringBuilder.ToString for an empty string, and once for using “”

The measurement action counts how many garbage collections occur when executing the lambda to create an empty string 100 million times.

I ran the code on a machine with VS 2010, one with VS 2012, and another machine with both installed.

The # of GCs is expected to be 0 because there are no objects being created. However, VS 2010 executes > 1000 GCs, because of this StringBuilder issue. The fix is evident in VS 2012, because the # GCs went to 0.

Start VS 2010 or 2012

File->New->Project->C#-> Windows Console Application. Paste in the code sample below.

Hit Ctrl-F5 to Run the code without debugging.

What’s really interesting was that when running the code in VS2010 on a machine with VS 2012 installed after VS 2010 was installed, the VS2010 installation got the fix as well.

/*

* On a machine with just VS 2010

String Builder Elapsed = 4.77 # GCs = 1017

DoubleQuote Elapsed = 1.67 # GCs = 0

On a machine with just VS 2012

String Builder Elapsed = 1.55 # GCs = 0

DoubleQuote Elapsed = 0.79 # GCs = 0 *

On a machine with Both 2010 and 2012, using the VS 2010

String Builder Elapsed = 1.42 # GCs = 0

DoubleQuote Elapsed = 0.64 # GCs = 0

*/

Exercises:

1. Try running this with Project->Properties->Build->Platform Target x64

2. Try using Release builds

See Also:

Increase the memory available to your tests

Examine the layout of managed strings in memory

<Code>

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Reflection;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var thisAsm = System.IO.Path.
                GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location);
            StringBuilder sb = new StringBuilder();
            var emptystr = sb.ToString();
            // define an action to measure the performance of some action
            Action<string, Action> actMeasure = (description, actTarget) =>
            {
                PerformanceCounter perfCounterGC = 
                    new PerformanceCounter(
                        ".NET CLR Memory", "# Gen 0 Collections", 
                        thisAsm);
                var nGCStart = (int)perfCounterGC.NextValue();
                var dtStart = DateTime.Now;
                for (int i = 0; i < 1000; i++)
                {
                    for (int j = 0; j < 100000; j++)
                    {
                        actTarget.Invoke();
                    }
                }
                var nGCs = (int)perfCounterGC.NextValue() - nGCStart;
                var elapsed = (DateTime.Now - dtStart).TotalSeconds;
                Console.WriteLine("{0,20} Elapsed = {1:n2}  # GCs = {2} ", 
                    description, elapsed, nGCs);
            };
            actMeasure.Invoke("String Builder", () =>
            {
                var str = sb.ToString();
            });
            actMeasure.Invoke("DoubleQuote", () =>
            {
                var str = "";
            });


            /*
             * Dev10
                String Builder Elapsed = 4.77  # GCs = 1017
                DoubleQuote Elapsed = 1.67  # GCs = 0
               Dev12
                String Builder Elapsed = 1.55  # GCs = 0
                DoubleQuote Elapsed = 0.79  # GCs = 0 * */
        }
    }
}

</Code>