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);
}
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.
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.
Ordinal comparisons are string comparisons in which each byte of each string is compared without linguistic interpretation; for example, "windows" does not match "Windows".
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. .
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With