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!