为什么String.Contains不直接调用最终的重载?

时间:2013-07-10 21:51:59

标签: c# string oop language-design contains

String.Contains方法在内部看起来像

public bool Contains(string value)
{
   return this.IndexOf(value, StringComparison.Ordinal) >= 0;
}

调用的IndexOf重载看起来像这样

public int IndexOf(string value, StringComparison comparisonType)
{
   return this.IndexOf(value, 0, this.Length, comparisonType);
}

这里再次调用最终重载,然后使用签名调用相关的CompareInfo.IndexOf方法

public int IndexOf(string value, int startIndex, int count, StringComparison comparisonType)

因此,调用最终的重载将是最快的(尽管在大多数情况下可能被认为是微优化)。

我可能会遗漏一些明显的东西,但为什么Contains方法不会直接调用最终的重载,因为在中间调用中没有进行其他工作,并且两个阶段都有相同的信息?

唯一的好处是,如果最终过载的签名发生变化,只需要进行一次更改(中间方法的更改),或者设计的更多内容是什么?

根据评论进行编辑(有关速度差异说明,请参阅更新2)

澄清我遇到的性能差异,以防我在某处犯了错误: 我运行this benchmark(循环5次以避免抖动偏差)并使用此扩展方法与String.Contains方法进行比较

public static bool QuickContains(this string input, string value)
{
   return input.IndexOf(value, 0, input.Length, StringComparison.OrdinalIgnoreCase) >= 0;
}

循环看起来像这样

for (int i = 0; i < 1000000; i++)
{
   bool containsStringRegEx = testString.QuickContains("STRING");
}
sw.Stop();
Console.WriteLine("QuickContains: " + sw.ElapsedMilliseconds);

在基准测试中,QuickContains似乎比我机器上的String.Contains快了约50%。

更新2(解释性能差异)

我在基准测试中发现了一些不公平的东西,这解释了很多。基准测试本身用于测量不区分大小写的字符串,但由于String.Contains只能执行区分大小写的搜索,因此包含了ToUpper方法。这会使结果产生偏差,而不是最终输出,但至少只是在非区分大小写的搜索中衡量String.Contains性能。

现在,如果我使用这种扩展方法

public static bool QuickContains(this string input, string value)
{
   return input.IndexOf(value, 0, input.Length, StringComparison.Ordinal) >= 0;
}

在2次重载StringComparison.Ordinal调用中使用IndexOf并删除ToUpperQuickContains方法实际上变得最慢。 IndexOfContains在性能方面几乎相同。很明显,ToUpper调用偏离了为什么ContainsIndexOf之间存在这种差异的结果。

不确定为什么QuickContains扩展方法变得最慢。 (可能与Contains具有[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]属性的事实有关?)。

问题仍然存在,为什么不直接调用4重载方法,但似乎性能不受影响的影响(正如Adrian和delnan在评论中指出的那样)。

1 个答案:

答案 0 :(得分:5)

自从我看过装配以来已经有一段时间了(几年),我对MSIL和JIT几乎一无所知,所以这将是一个很好的练习 - 无法抗拒,所以这里只是一点点,可能是多余的, 经验数据。 IndexOf重载是否内联?

这是一个很小的控制台应用程序:

class Program
{
    static void Main(string[] args)
    {
        "hello".Contains("hell");
    }
}

JIT在一个优化的Release版本中生成它,Any CPU,以32位运行。我缩短了地址,删除了一些不相关的行:

--- ...\Program.cs 
            "hello".Contains("hell");
[snip]
17  mov         ecx,dword ptr ds:[0320219Ch] ; pointer to "hello"
1d  mov         edx,dword ptr ds:[032021A0h] ; pointer to "hell"
23  cmp         dword ptr [ecx],ecx 
25  call        680A6A6C                     ; String.Contains()
[snip]

0x00000025处的call到此处:

<强> String.Contains

00  push        0                 ; startIndex = 0
02  push        dword ptr [ecx+4] ; count = this.Length (second DWORD of String)
05  push        4                 ; comparisonType = StringComparison.Ordinal
07  call        FF9655A4          ; String.IndexOf()
0c  test        eax,eax 
0e  setge       al                ; if (... >= 0)
11  movzx       eax,al 
14  ret 

果然,它似乎直接调用最后String.IndexOf重载,有四个参数:三个push ed;一个edxvalue:“地狱”); thisecx(“你好”)。要确认,这是call在0x00000005处的位置:

00  push        ebp 
01  mov         ebp,esp 
03  push        edi 
04  push        esi 
05  push        ebx 
06  mov         esi,ecx                  ; this ("hello")
08  mov         edi,edx                  ; value ("hell")
0a  mov         ebx,dword ptr [ebp+10h] 
0d  test        edi,edi                  ; if (value == null)
0f  je          00A374D0 
15  test        ebx,ebx                  ; if (startIndex < 0)
17  jl          00A374FB 
1d  cmp         dword ptr [esi+4],ebx    ; if (startIndex > this.Length)
20  jl          00A374FB 
26  cmp         dword ptr [ebp+0Ch],0    ; if (count < 0)
2a  jl          00A3753F 
[snip]

......这将是:

的主体
public int IndexOf(string value, 
                   int startIndex, 
                   int count, 
                   StringComparison comparisonType)
{
  if (value == null)
    throw new ArgumentNullException("value");
  if (startIndex < 0 || startIndex > this.Length)
    throw new ArgumentOutOfRangeException("startIndex",
             Environment.GetResourceString("ArgumentOutOfRange_Index"));
  if (count < 0 || startIndex > this.Length - count)
    throw new ArgumentOutOfRangeException("count",
             Environment.GetResourceString("ArgumentOutOfRange_Count"));
  ...
}