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
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.”:
Method | Time |
---|---|
equals operator | 10 milliseconds |
equals method | 8 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.”:
Method | Time |
---|---|
equals operator | 37 milliseconds |
equals method | 36 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;