包含比起以开头开始更快吗?

38

昨天一名顾问来访,不知何故谈到了字符串的话题。他提到,对于长度小于某个值的字符串来说,Contains 实际上比 StartsWith 更快。我必须要用自己的双眼看看,于是我写了一个小应用程序,果然,Contains 更快!

这怎么可能呢?

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);

输出:

726ms using Contains 
865ms using StartsWith

我已经尝试了更长的字符串!


2
两件事。尝试交换顺序以查看是否影响结果。然后,由于这是一个特定于实现的问题,请查看源代码,必要时通过Reflector查看。很可能Contains更加仔细地优化(可能使用本机代码),因为它经常被使用。 - Matthew Flaschen
5
微小优化很少有用。你正在比较一个长度可能为20个字符左右的字符串,在1000万次迭代中节省了约140毫秒。尝试使用更长的字符串或更有效的用例,看看是否得到相同的数字。 - Chris
11
你的时间测量存在缺陷。你应该使用 Stopwatch 对象来跟踪时间,而不是 DateTimes。如果你要使用 DateTimes,你至少应该使用 end.Subtract(start).TotalMilliseconds。 - Justin Niessner
时间似乎不会根据字符串长度而改变。但我也想问这是否重要?这些命令所花费的时间非常少,我看不出它会影响应用程序的性能。我宁愿看到较慢的StartsWith选项,也不想看到其他试图做同样事情的东西。 - Jeff Siver
6个回答

30

尝试使用StopWatch来测量速度,而不是使用DateTime进行检查。

使用System.DateTime.Now计时事件与Stopwatch的比较

我认为关键在于以下粗体部分:

Contains

此方法执行一个基于顺序的(区分大小写和与文化无关)比较。

StartsWith

此方法使用当前文化进行单词(区分大小写和与文化有关)比较。

我认为关键在于顺序比较,其相当于:

基于顺序的排序根据字符串中每个Char对象的数字值进行比较。顺序比较自动区分大小写,因为字符的小写和大写版本具有不同的代码点。但是,如果大小写在您的应用程序中不重要,则可以指定忽略大小写的顺序比较。这相当于使用不变的文化将字符串转换为大写,然后对结果执行顺序比较。

参考文献:

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

使用Reflector,您可以查看这两个代码:
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);
}

9
是的!这是正确的。正如丹尼尔在另一条评论中指出的那样,将StringComparison.Ordinal传递给StartsWith会使其比Contains更快。我刚试了一下,得到了以下结果: "使用Contains耗时748.3209毫秒 使用StartsWith耗时154.548毫秒"。 - StriplingWarrior
@StriplingWarrior,秒表在处理短进程时也不可靠。每次测试都会有差异。仅凭748与154的比较还不足以证明问题!因此,问题是,您尝试了多少次短进程测试? - usefulBee
1
@usefulBee:原始问题的代码重复调用方法一千万次,这将使我们进入数百毫秒的范畴。通常情况下,这已足够平滑掉没有I/O参与时的变化。这里有一个LINQPad脚本,它展示了在更强大的基准测试环境中类似的结果。 - StriplingWarrior
1
我刚刚运行了那个 Linqpad 脚本:Contains(): 1310,StartsWith():1630,Starts, WithOrdinal: 205。太棒了,Ordinal - CAD bloke

29

我明白了。这是因为StartsWith是与区域文化相关的,而Contains则不是。这本质上意味着StartsWith需要做更多的工作。

顺便说一下,在下面(已更正)的基准测试中,这是我在Mono上得到的结果:

1988.7906ms using Contains
10174.1019ms using StartsWith

我很乐意看到人们在 MS 上的结果,但我的主要观点是,如果正确地执行(并假设类似的优化),我认为 StartsWith 必须更慢:

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);

    }
}

2
非常好的猜测,但可能不是。他没有传递文化信息,而且这行代码在StartsWith的实现中:CultureInfo info = (culture == null) ? CultureInfo.CurrentCulture : culture; - Marc Bollinger
2
@Marc Bollinger - 你所展示的只是StartsWith是与文化相关的,这正是所说的。 - Lee
@Marc,没错。它使用当前文化。这是与文化相关的,并且一些文化依赖于相当复杂的规范化规则。 - Matthew Flaschen
10
StartsWith默认使用当前文化环境,这意味着比较需要检查类似于“æ”==“ae”的相等性。Contains不执行这些昂贵的检查。将StringComparison.Ordinal传递给StartsWith可以使其与Contains一样快。 - Daniel
2
为什么微软对不同的字符串方法采用不同的规则?这真是让人抓狂! - Qwertie

10

StartsWithContains在涉及到与文化有关的问题时,行为完全不同。

特别是,StartsWith返回true并不意味着Contains也返回true。只有当您真正知道自己在做什么时,才应该用一个替换另一个。

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
    }
}

3
我在Reflector中找到了一个可能的答案:
包含:
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");

还有一些重载方法,使得默认的文化是当前文化。

首先,如果字符串靠近开头,Ordinal 方法肯定更快,对吧?其次,这里有更多的逻辑可能会拖慢速度(尽管非常微不足道)。


1
我不同意 CultureInfo.CurrentCulture.CompareInfo.IsPrefix 是微不足道的。 - Matthew Flaschen
+1 -- 老实说,我并没有真正阅读它,只是在参考大量的代码而已 ;) - hackerhasid

1
这里是使用StartsWith和Contains的基准测试。 正如您所看到的,使用序数比较的StartsWith非常好,您应该注意每种方法分配的内存。
|                                   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 |

这是我的基准测试代码 https://gist.github.com/KieranMcCormick/b306c8493084dfc953881a68e0e6d55b


0

让我们看看ILSpy对这两个的说法...

public virtual int IndexOf(string source, string value, int startIndex, int count, CompareOptions options)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (value == null)
    {
        throw new ArgumentNullException("value");
    }
    if (startIndex > source.Length)
    {
        throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_Index"));
    }
    if (source.Length == 0)
    {
        if (value.Length == 0)
        {
            return 0;
        }
        return -1;
    }
    else
    {
        if (startIndex < 0)
        {
            throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_Index"));
        }
        if (count < 0 || startIndex > source.Length - count)
        {
            throw new ArgumentOutOfRangeException("count", Environment.GetResourceString("ArgumentOutOfRange_Count"));
        }
        if (options == CompareOptions.OrdinalIgnoreCase)
        {
            return source.IndexOf(value, startIndex, count, StringComparison.OrdinalIgnoreCase);
        }
        if ((options & ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth)) != CompareOptions.None && options != CompareOptions.Ordinal)
        {
            throw new ArgumentException(Environment.GetResourceString("Argument_InvalidFlag"), "options");
        }
        return CompareInfo.InternalFindNLSStringEx(this.m_dataHandle, this.m_handleOrigin, this.m_sortName, CompareInfo.GetNativeCompareFlags(options) | 4194304 | ((source.IsAscii() && value.IsAscii()) ? 536870912 : 0), source, count, startIndex, value, value.Length);
    }
}

看起来它也考虑了文化,但是默认设置。

public bool StartsWith(string value, StringComparison comparisonType)
{
    if (value == null)
    {
        throw new ArgumentNullException("value");
    }
    if (comparisonType < StringComparison.CurrentCulture || comparisonType > StringComparison.OrdinalIgnoreCase)
    {
        throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
    }
    if (this == value)
    {
        return true;
    }
    if (value.Length == 0)
    {
        return true;
    }
    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 && string.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;
    default:
        throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
    }

相比之下,我看到唯一相关的区别是额外的长度检查。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接