At one of the blogs I most frequent there was a post asking, “Where is the bug?”. In the comments section it was mentioned,

Use ToUpperInvariant rather than ToLowerInvariant when normalizing strings for comparison.

Sure enough if you go to the Microsoft page on it, it says

Use the String.ToUpperInvariant method instead of the String.ToLowerInvariant method when you normalize strings for comparison

Not wanting to blindly trust “Teh MAN” I threw together a quick testbed for testing many of the variants on doing string comparisons.

Here are the results (running 1 million comparisons) comparing
Guid.NewGuid().ToString() to “Now is the time for all good men to come to the aid of their country.”:

MethodTime
equals operator10 milliseconds
equals method8 milliseconds
CompareOrdinal(ToUpper)436 milliseconds
CompareOrdinal(ToUpperInvariant)876 milliseconds
CompareOrdinal(ToLower)418 milliseconds
CompareOrdinal(ToLowerInvariant)878 milliseconds
Compare(OrdinalIgnoreCase)29 milliseconds
Compare(InvariantCultureIgnoreCase)113 milliseconds
Compare(CurrentCultureIgnoreCase)135 milliseconds
Compare(ToUpperInvariant, Ordinal)889 milliseconds
Compare(ToLowerInvariant, Ordinal)889 milliseconds


If you read through Microsoft’s whole article you’ll see for case-insensitive ordinal comparisons they recommend

String.Compare(strA, strB, StringComparison.OrdinalIgnoreCase)

As you can see in the results this is borne out. It’s not inconceivable that these results could be significant, especially when doing a significant number of string comparisons.

The problem arises when I change what’s being compared to a string that is almost identical to the original.

Here are the results (running 1 million comparisons) comparing
“Now is the time for all good men to come to the aid of their countrt.” to
“Now is the time for all good men to come to the aid of their country.”:

MethodTime
equals operator37 milliseconds
equals method36 milliseconds
CompareOrdinal(ToUpper)502 milliseconds
CompareOrdinal(ToUpperInvariant)1111 milliseconds
CompareOrdinal(ToLower)494 milliseconds
CompareOrdinal(ToLowerInvariant)1120 milliseconds
Compare(OrdinalIgnoreCase)369 milliseconds
Compare(InvariantCultureIgnoreCase)176 milliseconds
Compare(CurrentCultureIgnoreCase)191 milliseconds
Compare(ToUpperInvariant, Ordinal)1096 milliseconds
Compare(ToLowerInvariant, Ordinal)1099 milliseconds


In both cases I ran the test (with code below this) several times and each time it gave similar results.

So what does this all mean? If you’re only doing a few thousand comparisons where case is an issue I wouldn’t worry about anything and just keep doing what you’re doing.
Other then that I can only recommend staying with Microsoft’s suggestion. While there was significant differences between the results, in general Microsoft’s suggestion would be more applicable without some sort of explicit knowledge of the data. I think if you’re going to analyze much beyond this you risk attempting to over-optimize.

Thanks,
Brian

Update 2024-02-17: Now you should use a package like BenchmarkDotNet for this instead of rolling it out on your own. I’d be curious to see what results I got in .NET Core 8 vs .NET Framework 4.0.

string myStringToCompare = "Now is the time for all good men to come to the aid of their countrt.";// Guid.NewGuid().ToString();
string originalString = "Now is the time for all good men to come to the aid of their country.";
string results = "";

Stopwatch watch = new Stopwatch();

int numComparisons = 1000000;
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (myStringToCompare == originalString) { }
}
watch.Stop();
results += "equals operator:" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (myStringToCompare.Equals(originalString)) { }
}
watch.Stop();
results += "nequals method:" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.CompareOrdinal(myStringToCompare.ToUpper(), originalString.ToUpper()) == 0) { }
}
watch.Stop();
results += "nCompareOrdinal(ToUpper):" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.CompareOrdinal(myStringToCompare.ToUpperInvariant(), originalString.ToUpperInvariant()) == 0) { }
}
watch.Stop();
results += "nCompareOrdinal(ToUpperInvariant):" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.CompareOrdinal(myStringToCompare.ToLower(), originalString.ToLower()) == 0) { }
}
watch.Stop();
results += "nCompareOrdinal(ToLower):" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.CompareOrdinal(myStringToCompare.ToLowerInvariant(), originalString.ToLowerInvariant()) == 0) { }
}
watch.Stop();
results += "nCompareOrdinal(ToLowerInvariant):" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.Compare(myStringToCompare, originalString, StringComparison.OrdinalIgnoreCase) == 0) { }
}
watch.Stop();
results += "nCompare(OrdinalIgnoreCase):" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.Compare(myStringToCompare, originalString, StringComparison.InvariantCultureIgnoreCase) == 0) { }
}
watch.Stop();
results += "nCompare(InvariantCultureIgnoreCase):" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.Compare(myStringToCompare, originalString, StringComparison.CurrentCultureIgnoreCase) == 0) { }
}
watch.Stop();
results += "nCompare(CurrentCultureIgnoreCase):" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.Compare(myStringToCompare.ToUpperInvariant(), originalString.ToUpperInvariant(), StringComparison.Ordinal) == 0) { }
}
watch.Stop();
results += "nCompare(ToUpperInvariant, Ordinal):" + (watch.ElapsedMilliseconds) + " milliseconds";

watch.Reset();
watch.Start();
for (int i = 0; i < numComparisons; i++)
{
    if (string.Compare(myStringToCompare.ToLowerInvariant(), originalString.ToLowerInvariant(), StringComparison.Ordinal) == 0) { }
}
watch.Stop();
results += "nCompare(ToLowerInvariant, Ordinal):" + (watch.ElapsedMilliseconds) + " milliseconds";

txtResults.Text = results;

Leave a Reply

Your email address will not be published. Required fields are marked *

FormatException

928 East Plymouth Drive Asbury Park, NJ 07712