Contains is faster than StartsWith?
Solution 1
Try using StopWatch
to measure the speed instead of DateTime
checking.
Stopwatch vs. using System.DateTime.Now for timing events
I think the key is the following the important parts bolded:
Contains
:
This method performs an ordinal (case-sensitive and culture-insensitive) comparison.
StartsWith
:
This method performs a word (case-sensitive and culture-sensitive) comparison using the current culture.
I think the key is the ordinal comparison which amounts to:
An ordinal sort compares strings based on the numeric value of each Char object in the string. An ordinal comparison is automatically case-sensitive because the lowercase and uppercase versions of a character have different code points. However, if case is not important in your application, you can specify an ordinal comparison that ignores case. This is equivalent to converting the string to uppercase using the invariant culture and then performing an ordinal comparison on the result.
References:
http://msdn.microsoft.com/en-us/library/system.string.aspx
http://msdn.microsoft.com/en-us/library/dy85x1sa.aspx
http://msdn.microsoft.com/en-us/library/baketfxw.aspx
Using Reflector you can see the code for the two:
public bool Contains(string value)
{
return (this.IndexOf(value, StringComparison.Ordinal) >= 0);
}
public bool StartsWith(string value, bool ignoreCase, CultureInfo culture)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
if (this == value)
{
return true;
}
CultureInfo info = (culture == null) ? CultureInfo.CurrentCulture : culture;
return info.CompareInfo.IsPrefix(this, value,
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
}
Solution 2
I figured it out. It's because StartsWith
is culture-sensitive, while Contains is not. That inherently means StartsWith
has to do more work.
FWIW, here are my results on Mono with the below (corrected) benchmark:
1988.7906ms using Contains
10174.1019ms using StartsWith
I'd be glad to see people's results on MS, but my main point is that correctly done (and assuming similar optimizations), I think StartsWith
has to be slower:
using System;
using System.Diagnostics;
public class ContainsStartsWith
{
public static void Main()
{
string str = "Hello there";
Stopwatch s = new Stopwatch();
s.Start();
for (int i = 0; i < 10000000; i++)
{
str.Contains("H");
}
s.Stop();
Console.WriteLine("{0}ms using Contains", s.Elapsed.TotalMilliseconds);
s.Reset();
s.Start();
for (int i = 0; i < 10000000; i++)
{
str.StartsWith("H");
}
s.Stop();
Console.WriteLine("{0}ms using StartsWith", s.Elapsed.TotalMilliseconds);
}
}
Solution 3
StartsWith
and Contains
behave completely different when it comes to culture-sensitive issues.
In particular, StartsWith
returning true
does NOT imply Contains
returning true
. You should replace one of them with the other only if you really know what you are doing.
using System;
class Program
{
static void Main()
{
var x = "A";
var y = "A\u0640";
Console.WriteLine(x.StartsWith(y)); // True
Console.WriteLine(x.Contains(y)); // False
}
}
Solution 4
I twiddled around in Reflector and found a potential answer:
Contains:
return (this.IndexOf(value, StringComparison.Ordinal) >= 0);
StartsWith:
...
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:
return ((this.Length >= value.Length) && (nativeCompareOrdinalEx(this, 0, value, 0, value.Length) == 0));
case StringComparison.OrdinalIgnoreCase:
return ((this.Length >= value.Length) && (TextInfo.CompareOrdinalIgnoreCaseEx(this, 0, value, 0, value.Length, value.Length) == 0));
}
throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
And there are some overloads so that the default culture is CurrentCulture.
So first of all, Ordinal will be faster (if the string is close to the beginning) anyway, right? And secondly, there's more logic here which could slow things down (although so so trivial)
Solution 5
Here is a benchmark of using StartsWith vs Contains. As you can see, StartsWith using ordinal comparison is pretty good, and you should take note of the memory allocated for each method.
| Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------------------------------- |-------------:|-----------:|-------------:|-------------:|----------:|------:|------:|----------:|
| EnumEqualsMethod | 1,079.67 us | 43.707 us | 114.373 us | 1,059.98 us | 1019.5313 | - | - | 4800000 B |
| EnumEqualsOp | 28.15 us | 0.533 us | 0.547 us | 28.34 us | - | - | - | - |
| ContainsName | 1,572.15 us | 152.347 us | 449.198 us | 1,639.93 us | - | - | - | - |
| ContainsShortName | 1,771.03 us | 103.982 us | 306.592 us | 1,749.32 us | - | - | - | - |
| StartsWithName | 14,511.94 us | 764.825 us | 2,255.103 us | 14,592.07 us | - | - | - | - |
| StartsWithNameOrdinalComp | 1,147.03 us | 32.467 us | 93.674 us | 1,153.34 us | - | - | - | - |
| StartsWithNameOrdinalCompIgnoreCase | 1,519.30 us | 134.951 us | 397.907 us | 1,264.27 us | - | - | - | - |
| StartsWithShortName | 7,140.82 us | 61.513 us | 51.366 us | 7,133.75 us | - | - | - | 4 B |
| StartsWithShortNameOrdinalComp | 970.83 us | 68.742 us | 202.686 us | 1,019.14 us | - | - | - | - |
| StartsWithShortNameOrdinalCompIgnoreCase | 802.22 us | 15.975 us | 32.270 us | 792.46 us | - | - | - | - |
| EqualsSubstringOrdinalCompShortName | 4,578.37 us | 91.567 us | 231.402 us | 4,588.09 us | 679.6875 | - | - | 3200000 B |
| EqualsOpShortNametoCharArray | 1,937.55 us | 53.821 us | 145.508 us | 1,901.96 us | 1695.3125 | - | - | 8000000 B |
Here is my benchmark code https://gist.github.com/KieranMcCormick/b306c8493084dfc953881a68e0e6d55b
hackerhasid
hacker, founder of bargeapp.com - load testing for developers
Updated on August 03, 2022Comments
-
hackerhasid almost 2 years
A consultant came by yesterday and somehow the topic of strings came up. He mentioned that he had noticed that for strings less than a certain length,
Contains
is actually faster thanStartsWith
. I had to see it with my own two eyes, so I wrote a little app and sure enough,Contains
is faster!How is this possible?
DateTime start = DateTime.MinValue; DateTime end = DateTime.MinValue; string str = "Hello there"; start = DateTime.Now; for (int i = 0; i < 10000000; i++) { str.Contains("H"); } end = DateTime.Now; Console.WriteLine("{0}ms using Contains", end.Subtract(start).Milliseconds); start = DateTime.Now; for (int i = 0; i < 10000000; i++) { str.StartsWith("H"); } end = DateTime.Now; Console.WriteLine("{0}ms using StartsWith", end.Subtract(start).Milliseconds);
Outputs:
726ms using Contains 865ms using StartsWith
I've tried it with longer strings too!
-
uKolka almost 14 yearsReally good guess, but likely not. He's not passing in the culture, and this line is in the implementation of StartsWith:
CultureInfo info = (culture == null) ? CultureInfo.CurrentCulture : culture;
-
Lee almost 14 years@Marc Bollinger - All you've shown there is that StartsWith is culture-sensitive, which is the claim.
-
Matthew Flaschen almost 14 years@Marc, right. It's using the current culture. That's culture-sensitive, and some cultures rely on quite complex normalization rules.
-
Daniel almost 14 yearsStartsWith uses CurrentCulture by default, which means the comparison has to check for equalities like "æ"=="ae". Contains doesn't do those expensive checks. Pass StringComparison.Ordinal to StartsWith to make it as fast as Contains.
-
Matthew Flaschen almost 14 yearsI don't agree that
CultureInfo.CurrentCulture.CompareInfo.IsPrefix
is trivial. -
StriplingWarrior almost 14 yearsYes! This is correct. As Daniel pointed out in another comment, passing StringComparison.Ordinal to StartsWith will make StartsWith much faster than Contains. I just tried it and got "748.3209ms using Contains 154.548ms using StartsWith"
-
Qwertie almost 14 yearsWhy does Microsoft pick different rules for different string methods? It's maddening!
-
hackerhasid almost 14 years+1 -- I didn't really read it to be honest, I was just referring to the sheer amount of code ;)
-
usefulBee over 7 years@StriplingWarrior, Stopwatch is not reliable either with short processes. There will always be variations with each test. Getting 748 vs 154...is not enough evidence! So the question is, how many times you tried your short process test??
-
StriplingWarrior over 7 years@usefulBee: The original question's code repeats the method call ten million times, which puts us into the hundreds of milliseconds. That's usually enough to smooth out the variations when there's no I/O involved. Here's a LINQPad script that shows similar results in a more robust benchmark test bed.
-
CAD bloke almost 7 yearsI just ran that Linqpad script:
Contains()
: 1310,StartsWith()
:1630,Starts, WithOrdinal
: 205. YayOrdinal