Contains is faster than StartsWith?

29,231

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

Share:
29,231
hackerhasid
Author by

hackerhasid

hacker, founder of bargeapp.com - load testing for developers

Updated on August 03, 2022

Comments

  • hackerhasid
    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 than StartsWith. 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
    uKolka almost 14 years
    Really 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
    Lee almost 14 years
    @Marc Bollinger - All you've shown there is that StartsWith is culture-sensitive, which is the claim.
  • Matthew Flaschen
    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
    Daniel almost 14 years
    StartsWith 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
    Matthew Flaschen almost 14 years
    I don't agree that CultureInfo.CurrentCulture.CompareInfo.IsPrefix is trivial.
  • StriplingWarrior
    StriplingWarrior almost 14 years
    Yes! 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
    Qwertie almost 14 years
    Why does Microsoft pick different rules for different string methods? It's maddening!
  • hackerhasid
    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
    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
    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
    CAD bloke almost 7 years
    I just ran that Linqpad script: Contains(): 1310, StartsWith():1630, Starts, WithOrdinal: 205. Yay Ordinal