Search code examples
c#stringstring-comparisonstartswith

Performance of String.StartsWith using StringComparison.OrdinalIgnoreCase


I ran into a strange performance "artifact" with String.StartsWith.

It appears that String.StartsWith using OrdinalIgnoreCase is faster than using String.StartsWith without specifying a StringComparison. (2-4x faster)

However, checking equality is faster using String.Equals with no StringComparison than when using OrdinalIgnoreCase. (Though all are roughly the same speed)

The question is why? Why do they perform differently in the two cases?

Here is the code I was using:

    public static void Test()
    {
        var options = new[] { "asd/klfe", "qer/jlkfe", "p33/ji", "fkjlfe", "asd/23", "bleash", "quazim", "ujv/3", "jvd/kfl" };
        Random r;

        const int trialSize = 100000;
        const int trials = 1000;
        Stopwatch swEqOp = new Stopwatch();
        Stopwatch swEq = new Stopwatch();
        Stopwatch swEqOrdinal = new Stopwatch();
        Stopwatch swStartsWith = new Stopwatch();
        Stopwatch swStartsWithOrdinal = new Stopwatch();
        for (int i = 0; i < trials; i++)
        {
            {
                r = new Random(1);
                swEqOp.Start();
                for (int j = 0; j < trialSize; j++)
                {
                    bool result = options[r.Next(options.Length)] == "asd/klfe";
                }
                swEqOp.Stop();
            }

            {
                r = new Random(1);
                swEq.Start();
                for (int j = 0; j < trialSize; j++)
                {
                    bool result = string.Equals(options[r.Next(options.Length)], "asd/klfe");
                }
                swEq.Stop();
            }

            {
                r = new Random(1);
                swEqOrdinal.Start();
                for (int j = 0; j < trialSize; j++)
                {
                    bool result = string.Equals(options[r.Next(options.Length)], "asd/klfe", StringComparison.OrdinalIgnoreCase);
                }
                swEqOrdinal.Stop();
            }

            {
                r = new Random(1);
                swStartsWith.Start();
                for (int j = 0; j < trialSize; j++)
                {
                    bool result = options[r.Next(options.Length)].StartsWith("asd/");
                }
                swStartsWith.Stop();
            }

            {
                r = new Random(1);
                swStartsWithOrdinal.Start();
                for (int j = 0; j < trialSize; j++)
                {
                    bool result = options[r.Next(options.Length)].StartsWith("asd/",StringComparison.OrdinalIgnoreCase);
                }
                swStartsWithOrdinal.Stop();
            }

        }

        //DEBUG with debugger attached. Release without debugger attached. AnyCPU both cases.

        //DEBUG : 1.54      RELEASE : 1.359
        Console.WriteLine("Equals Operator: " + swEqOp.ElapsedMilliseconds / 1000d);

        //DEBUG : 1.498      RELEASE : 1.349  <======= FASTEST EQUALS
        Console.WriteLine("String.Equals: " + swEq.ElapsedMilliseconds / 1000d);

        //DEBUG : 1.572      RELEASE : 1.405
        Console.WriteLine("String.Equals OrdinalIgnoreCase: " + swEqOrdinal.ElapsedMilliseconds / 1000d);

        //DEBUG : 14.234      RELEASE : 9.914
        Console.WriteLine("String.StartsWith: " + swStartsWith.ElapsedMilliseconds / 1000d);

        //DEBUG : 7.956      RELEASE : 3.953  <======= FASTEST StartsWith
        Console.WriteLine("String.StartsWith OrdinalIgnoreCase: " + swStartsWithOrdinal.ElapsedMilliseconds / 1000d);

    }

Solution

  • So unlike String.StartsWith (as pointed out by Enigmativity), String.Equals does not use any StringComparison by default if none is specified. Instead it uses its own custom implementation, which you can see at the below link: https://referencesource.microsoft.com/#mscorlib/system/string.cs,11648d2d83718c5e

    This is slightly faster than the Ordinal Comparison.

    But it is important to note that if you want consistency between your comparisons, use both String.Equals and String.StartsWith with a StringComparison, or they are not operating as you'd expect.