What’s faster: string.Equals or string.Compare?

I just realized I was so busy lately that I haven’t blogged for a while!

Here’s a quiz that left me clueless for some time (courtesy of our C# MVP Ahmed Ilyas):

 using System;
using System.Diagnostics;
 
public class Examples
{
    public static void Main()
    {
        string stringToTest = "Hello";
 
        Stopwatch equalsTimer = new Stopwatch();
        equalsTimer.Start();
        stringToTest.Equals("hello", StringComparison.OrdinalIgnoreCase);
        equalsTimer.Stop();
        Console.WriteLine("Equals Timer: {0}", equalsTimer.Elapsed);
 
        Stopwatch compareTimer = new Stopwatch();
        compareTimer.Start();
        String.Compare(stringToTest, "hello", StringComparison.OrdinalIgnoreCase);
        compareTimer.Stop();
        Console.WriteLine("Compare Timer: {0}", compareTimer.Elapsed);
    }
}

On my machine, this prints out:

Equals Timer: 00:00:00.0009247

Compare Timer: 00:00:00.0000012

We looked at the source code of string.Equals and string.Compare and it was essentially the same (modulo very minor details which shouldn’t cause issues).

So what’s wrong? Why would the first call be 770 times slower than the second one? Jitting? No. Cache hit/miss? No.

After a while, we figured it out [UPDATE: So I thought! ]. The first method is a virtual instance method, so a callvirt is emitted by the compiler:

callvirt instance bool [mscorlib]System.String::Equals(string, valuetype [mscorlib]System.StringComparison)

While the second method is a static one, so the call instruction is used instead:

call int32 [mscorlib]System.String::Compare(string, string, valuetype [mscorlib]System.StringComparison)

In this case, the method body was insignificant compared to the costs of doing virtual dispatch vs. a direct call. If you’d measure this in a loop of 1000000, the results will average out. So will they average out if you compare long strings, when the method body execution time dwarfs the call costs.

UPDATE: As always, Kirill jumps to conclusions too fast. Ahmed pointed out that if you swap the order of the calls, then the results are way different again! So it’s not the callvirt cost. Still puzzled, maybe it IS the JITter compiling the BCL code for the two method bodies.

Interesting...