Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

    }
like image 680
MineR Avatar asked Aug 24 '17 05:08

MineR


People also ask

What does StringComparison OrdinalIgnoreCase do?

OrdinalIgnoreCase. The StringComparison has the OrdinalIgnoreCase property and treats the characters in the strings to compare as if they were converted to uppercase (using the conventions of the invariant culture) and then it performs a simple byte comparison and it is independent of language.

What is difference between InvariantCultureIgnoreCase and OrdinalIgnoreCase?

InvariantCultureIgnoreCase uses comparison rules based on english, but without any regional variations. This is good for a neutral comparison that still takes into account some linguistic aspects. OrdinalIgnoreCase compares the character codes without cultural aspects.

What is string ordinal comparison?

Ordinal comparisons are string comparisons in which each byte of each string is compared without linguistic interpretation; for example, "windows" does not match "Windows".

What is StringComparison CurrentCultureIgnoreCase?

The StringComparer returned by the CurrentCultureIgnoreCase property can be used when strings are linguistically relevant but their case is not. For example, if strings are displayed to the user but case is unimportant, culture-sensitive, case-insensitive string comparison should be used to order the string data. .


2 Answers

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
like image 182
Enigmativity Avatar answered Nov 01 '22 17:11

Enigmativity


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.

like image 34
MineR Avatar answered Nov 01 '22 19:11

MineR