String.Empty versus "" – The real deal

This is one topic that keeps popping up every now and then. A lot of people seem to be curious about the performance implications of using one versus the other, and not unexpectedly, a lot of different answers come up.

To settle it once for all, here’s a small program that uses both of them.


namespace ConsoleApplication
{
class Program
{
static void Main()
{
Method1();
Method2();
Console.ReadLine();
Method1();
Method2();
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void Method1()
{
string s = "";
DoNothing(s);
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void Method2()
{
string s = string.Empty;
DoNothing(s);
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void DoNothing(string s)
{
Console.WriteLine(s);
}
}
}

As you can see above, Method1 and Method2 call DoNothing, passing “” and string.Empty respectively. Now how do we compare these two? Easy, just look at the jitted code – after all, that’s what is going to run on the machine.

We’ll use Windbg to disassemble the code after the JITter did its job. And if you’re wondering why Method1 and Method2 have been called twice, now you know why – the second time around, we can look at the JITted code that was generated as part of the first time execution of each of those methods.

So breaking into the debugger at Console.ReadLine() and dumping the method descriptors for Program


0:003> !Name2EE Sample.exe!ConsoleApplication.Program
...
MethodTable: 002a3048

0:003> !DumpMT -MD 002a3048
...
--------------------------------------
Entry MethodDesc JIT Name
...
00860070 002a3020 JIT ConsoleApplication.Program.Main()
008600a8 002a3028 JIT ConsoleApplication.Program.Method1()
00860100 002a3030 JIT ConsoleApplication.Program.Method2()
008600c8 002a3038 JIT ConsoleApplication.Program.DoNothing(System.String)
002ac021 002a3040 NONE ConsoleApplication.Program..ctor()

shows the method descriptors for all the methods in that class. Now, using !u to disassemble code for Method1


0:003> !u 002a3028
Normal JIT generated code
ConsoleApplication.Program.Method1()
Begin 008600a8, size d
008600a8 8b0d3c30e402 mov ecx,dword ptr ds:[2E4303Ch] ("")
008600ae ff158c302a00 call dword ptr ds:[2A308Ch] (ConsoleApplication.Program.DoNothing(System.String), mdToken: 06000004)
008600b4 c3 ret

and then for Method2


0:003> !u 002a3030
Normal JIT generated code
ConsoleApplication.Program.Method2()
Begin 00860100, size c
00860100 8b0d2c10e402 mov ecx,dword ptr ds:[2E4102Ch] ("")
00860106 e8bdffffff call 008600c8 (ConsoleApplication.Program.DoNothing(System.String), mdToken: 06000004)
0086010b c3 ret

shows that, surprise surprise, the generated assembly code is identical. The only difference is the address referenced in the mov instruction.

So there you have it – the JITter generates the same code whether you use String.Empty or “”, and there should no difference in performance.

PS : It’s interesting to note that the two addresses are different, even though the string contents are the same. Address 2E4303Ch in the disassembly for Method1 is the address referring to an interned string object with the content “”. You can verify that by creating another string variable initialized to “” and disassembling that code – the same address (2E4303Ch) will be referenced there as well. What’s surprising is that the address referenced in Method2 is different – which means the string object referenced by string.Empty is not the interned object. Wonder why – maybe because it’s ngen’ned code?

 

3 thoughts on “String.Empty versus "" – The real deal”

  1. No surprise as the “call” instruction is not the real assigment you were looking for to compare performance. You should be continuing drilling down intro the JIT-generated code to find out what happens!

  2. Well no, it’s not the call instruction, but the mov instruction just before it that is the assignment. It moves the reference to the string to ecx and calls DoNothing.

  3. OOPS!!! Sorry matey :)

    Anyway, the constant literal case (“”) goes through internal AppDomain (well Module actually) hash-table (string interning) which is slower during JIT compilation. This is supposed to happen once for every “” usage unless exclusively optimised which makes it implementation-specific though.

    At the other hand, the static field alternative (String.Empty) goes through the same process only once for the whole AppDomain speeding up JIT compilation!

    Both gains and pains are not enough making you ponder; there are lots of performance gains available out there by clever architectural decisions and …

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>