6

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);

    }
mmushtaq
  • 3,430
  • 7
  • 30
  • 47
MineR
  • 2,144
  • 12
  • 18

2 Answers2

2

It seems that the implementation is different in public Boolean StartsWith(String value, StringComparison comparisonType):

        switch (comparisonType) {
            case StringComparison.CurrentCulture:
                return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, CompareOptions.None);

            case StringComparison.CurrentCultureIgnoreCase:
                return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, CompareOptions.IgnoreCase); 

            case StringComparison.InvariantCulture:
                return CultureInfo.InvariantCulture.CompareInfo.IsPrefix(this, value, CompareOptions.None); 

            case StringComparison.InvariantCultureIgnoreCase:
                return CultureInfo.InvariantCulture.CompareInfo.IsPrefix(this, value, CompareOptions.IgnoreCase);

            case StringComparison.Ordinal:
                if( this.Length < value.Length) { 
                    return false; 
                }
                return (nativeCompareOrdinalEx(this, 0, value, 0, value.Length) == 0); 

            case StringComparison.OrdinalIgnoreCase:
                if( this.Length < value.Length) {
                    return false; 
                }

                return (TextInfo.CompareOrdinalIgnoreCaseEx(this, 0, value, 0, value.Length, value.Length) == 0); 

            default: 
                throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
        }

The default comparison used is:

#if FEATURE_CORECLR
                              StringComparison.Ordinal);
#else
                              StringComparison.CurrentCulture); 
#endif
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • I could understand why it would be faster with Ordinal - I just don't get why String.Equals behaves differently... – MineR Aug 24 '17 at 05:35
  • OK, so upon looking at String.Equals, it does not actually use a StringComparison if none is specified - instead it uses a specific implementation. – MineR Aug 25 '17 at 06:32
1

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.

MineR
  • 2,144
  • 12
  • 18