Local const strings - do they give better performance?

I just installed Resharper 4 (www.jetbrains.com) on my dev machine, and aside from the usual hints etc, there are some new ones, most notably the way it asks to insert a const string locally, for example:

  private void ShowMessage()
  {
    string msg = "Hello!!";
    Console.WriteLine( msg );
  }
ReSharper suggests should be replaced with:
  private void ShowMessage()
  {
    const string msg = "Hello!!";
    Console.WriteLine( msg );
  }
Now, I've never done that in my code in the past, but, presumably there is some performance gain? Time to investigate!

So, lets write a small ConsoleApp:

class TestConst
{
    private static void WriteMessage()
    {
        string msg = "Hello!";
        System.Console.WriteLine( msg );
    }
 
    private static void WriteMessage_const()
    {
        const string msg = "Hello!";
        System.Console.WriteLine( msg );
    }
 
    static void Main()
    {
        WriteMessage();
        WriteMessage_const();
        System.Console.ReadLine();
    }
}

Build and run it: get the expected output of:

    Hello!
    Hello!

So far so good, now let's open the code in ILDasm.

    ildasm -adv TestConst.exe



Which gives us:



If we double click on the two methods, WriteMessage and WriteMessage_const we can see the ILCode:

So, for the NON-Const-ified method we have:

    .method private hidebysig static void  WriteMessage() cil managed
    {
      // Code size       15 (0xf)
      .maxstack  1
      .locals init ([0] string msg)
      IL_0000:  nop
      IL_0001:  ldstr      "Hello!"
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
      IL_0008:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_000d:  nop
      IL_000e:  ret
    } // end of method TestConst::WriteMessage

and for the Const-ified method:

    .method private hidebysig static void  WriteMessage_const() cil managed
    {
      // Code size       13 (0xd)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      "Hello!"
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_000b:  nop
      IL_000c:  ret
    } // end of method TestConst::WriteMessage_const

Clearly there is a size difference here, but if we ignore the code that's the same, we end up with 2 extra lines in the Non-Const method:

      IL_0001:  ldstr      "Hello!"
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
    IL_0008:  call       void [mscorlib]System.Console::WriteLine(string)

What do these calls indicate?
    Line 1. we have the ldstr "Hello!" call, which loads the literal string "Hello!" onto the stack.
    Line 2. Next is the 'stloc.0' call, this pops the string from the stack and stores it in the local variable '0'.
    Line 3. ldloc.0 loads the '0' variable onto the evaluation stack,
    Line 4. Here we call the Console.WriteLine method with the evaluation stack filled with arguments.

So what can we get from this? Well, firstly, not using 'const' causes 2 extra lines of IL to be generated, which in this case is tiny and insignificant. But maybe we should scale it up and do some timed analysis of large numbers of calls....

Just quickly before jumping into the multi-call tests - some people might be asking whether a release build will 'inline' the code? The short answer (and indeed *only* answer) is no, the ILCode looks a bit tidier, but the stloc.0 and ldloc.0 calls are still made.

Right, onto the multi-call tests...

We'll need to modify the methods slighty, calling 'System.Console.WriteLine' is expensive at the best of times, and we're only really interested in seeing performance gained from the actual calls...

So, we'll make the methods call a 'DoNothing' method...

    private static void DoNothing( string msg )
    {
        var tempStr = msg;
    }

Next, we'll need a test class!

class Tester
{
    public delegate void NoArgsDelegate();
 
    public static System.String AverageTest(int testRepetitions, int averageRepetitions, params NoArgsDelegate[] toTest)
    {
        var output = new System.Text.StringBuilder();
 
        foreach ( var noArgsDelegate in toTest )
        {
            double totalMs = 0;
            for( var i = 0; i < averageRepetitions; i++ )
            {
                var start = System.DateTime.Now;
                for( var j = 0; j < testRepetitions; j++ )
                    noArgsDelegate();
 
                var ms = ( System.DateTime.Now - start ).TotalMilliseconds;
                totalMs += ms;
               
                System.Console.Write( "Sleeping..." );
                System.Threading.Thread.Sleep( 300 );
                System.Console.WriteLine( "...Done (" + ms +  ")" );
            }
 
            output.Append( noArgsDelegate.Method.Name ).Append( ": " ).Append( totalMs / averageRepetitions ).Append( System.Environment.NewLine );  
        }
       
        return output.ToString();
    }
}

In here we have (firstly) the delegate we're going to call in the test class, and then the method we'll call. One thing that might strike as odd is the 'Thread.Sleep' call, this is purely because otherwise the code executed so fast the results were all the same (admiteddly there could be other reasons!). Anyhews, the code should be fairly obvious, and it's use is simple:

    var results = Tester.AverageTest(100000000, 5, WriteMessage, WriteMessage_const);
   
Soooo... after a brief modification to the Main method:

    static void Main()
    {
        var r1 = Tester.AverageTest( 100000000, 5, WriteMessage, WriteMessage_const );
        var r2 = Tester.AverageTest( 100000000, 10, WriteMessage, WriteMessage_const );
        var r3 = Tester.AverageTest( 100000000, 15, WriteMessage, WriteMessage_const );
        var r4 = Tester.AverageTest( 100000000, 20, WriteMessage, WriteMessage_const );
 
        System.Console.WriteLine( "Average over 5" + System.Environment.NewLine + r1 );
        System.Console.WriteLine( "Average over 10" + System.Environment.NewLine + r2 );
        System.Console.WriteLine( "Average over 15" + System.Environment.NewLine + r3 );
        System.Console.WriteLine( "Average over 20" + System.Environment.NewLine + r4 );
 
        System.Console.WriteLine( "Press ENTER to exit." );
        System.Console.ReadLine();
    }

We get the following results:
  
    Average over 5
    WriteMessage: 951.7403
    WriteMessage_const: 898.69248

    Average over 10
    WriteMessage: 953.30053
    WriteMessage_const: 900.25271

    Average over 15
    WriteMessage: 952.780453333333
    WriteMessage_const: 912.214473333333

    Average over 20
    WriteMessage: 956.42099
    WriteMessage_const: 908.833975

The 'const' results coming out consistently lower (though not by much), roughly 50ms faster over 100,000,000 iterations. Of course, we're only setting one variable here anyhow, what about setting two? i.e. using:

    private static void WriteMessage()
    {
        string msg = "Hello!";
        string msg2 = "Hello2!";
        DoNothing( msg );
        DoNothing( msg2 );
    }
 
    private static void WriteMessage_const()
    {
        const string msg = "Hello!";
        const string msg2 = "Hello2!";
        DoNothing( msg );
        DoNothing( msg2 );
    }

instead...

    Average over 5
    WriteMessage: 1195.13618
    WriteMessage_const: 1195.13618

    Average over 10
    WriteMessage: 1192.01572
    WriteMessage_const: 1188.89526

    Average over 15
    WriteMessage: 1195.13618
    WriteMessage_const: 1192.01572

    Average over 20
    WriteMessage: 1188.89526
    WriteMessage_const: 1193.57595

Pretty much the *same* this time... with the consts an average of 2ms *slower* than the non-consts.

What does this mean?

Well - ahem - pretty much nothing; in general use - using const to define 'strings' locally will only give a marginal improvement at best. Perhaps other types defined as consts will have a bigger effect, in particular larger items -- hmm, as I wrote that, I thought I'd check a longer string (700+ characters) and I get:

    Average over 5
    WriteMessage: 1007.17452
    WriteMessage_const: 951.22038

    Average over 10
    WriteMessage: 1005.52881
    WriteMessage_const: 957.80322

    Average over 15
    WriteMessage: 1003.8831
    WriteMessage_const: 956.70608

    Average over 20
    WriteMessage: 1000.7008
    WriteMessage_const: 922.41069

Which is still around the 50ms faster result...
Hmmm

Right, well, back to the 'analysis'. From an IL code perspective, it's clearly more optimised to use 'const' to define the local strings, we have 2 less operations, but what this actually equates to in terms of performance is pretty negligable, if you have something like Resharper which points them out, then you may as well convert to consts, but otherwise it's probably gonna use up more time adding const than you'll save in the lifetime of your app!

Meh!

Print | posted @ Friday, July 25, 2008 3:46 PM

Comments on this entry:

Gravatar # re: Local const strings - do they give better performance?
by Robert at 7/28/2008 7:32 AM

Maybe it's not about performance but code clarity. By using the const keyword you are telling the reader of the code that the string is constant and its value won't change.

Gravatar # re: Local const strings - do they give better performance?
by Mark Brackett at 7/28/2008 2:36 PM

Of course - the real reason for doing this is not premature optimization, but for clarity. A const (or readonly) variable is clearly not modified.
In the context of a short method, this may not be a huge deal - but is still a best practice IMHO.
Gravatar # re: Local const strings - do they give better performance?
by Chris at 7/28/2008 4:18 PM

1 post and 2 comments about clarity, and to be honest, I didn't think about it from that perspective.

I can see both of your points of view, and it's more because it was something I'd never really thought about.. Clarity would seem to be the overriding reason for the hint in Resharper!
Gravatar # re: Local const strings - do they give better performance?
by Chris Carter at 7/29/2008 7:35 PM

I'm pretty sure the reason why you declare your strings const (and any fields that you can, readonly) is an attempt to make your data as immutable as possible. Immutable data structures provide thread-safety and reduce the number of possible errors down the road due to unexpected data changes.
Gravatar # re: Local const strings - do they give better performance?
by Eddie at 7/30/2008 11:34 PM

The reason why your double const test didn't do any better is because you have 2 variables on the stack, and you wanted to use the lowest one first.

so if you were to write your test as:
private static void WriteMessage_const() {
const string msg = "Hello!";
DoNothing( msg );
const string msg2 = "Hello2!";
DoNothing( msg2 );
}
you are only loading the stack with what is needed for the next method call.

In either case, const is definately suggested there for clarity.
Gravatar # re: Local const strings - do they give better performance?
by Ed at 8/1/2008 5:05 PM

The performance effect in your tests is small, but look at it another way; The current JIT implementation will only inline methods with <32 bytes of IL. The saving of two instructions could mean the difference between inlining or not :)
Gravatar # re: Local const strings - do they give better performance?
by Chris at 8/2/2008 12:13 PM

Thanks for the comments guys, all of them informative and useful!
Gravatar # re: Local const strings - do they give better performance?
by Stevan at 9/8/2008 9:41 AM

I've been using const strings in my applications for as long as I can remember. The performance increase is due to the string being read-only, meaning no back-end execution has to be done that validates any unintentional data changes will may or may not affect the applications execution.

I usually have a helper class that handles this. Beautiful code, by the way!
Post A Comment
Title:
Name:
Email:
Comment:
Verification: