13

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、中間呼び出しで他の作業が行われず、両方の段階で同じ情報が利用できることを考えると、メソッドが最終的なオーバーロードを直接呼び出さないのはなぜですか?

最終的なオーバーロードの署名が変更された場合、1 つの変更 (中間メソッドの変更) のみを行う必要があるという唯一の利点はありますか、それともそれ以上の設計がありますか?

コメントから編集 (速度の違いの説明については、更新 2 を参照してください)

どこかで間違いを犯した場合に得られるパフォーマンスの違いを明確にするために、このベンチマークString.Containsを実行し (ジッター バイアスを避けるために 5 回ループしました)、この拡張メソッドを使用してメソッドと比較しました。

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よりも約 50% 高速に見えます。String.Contains

更新 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;
}

StringComparison.Ordinal2 オーバーロードIndexOf呼び出しと removeで使用するToUpperと、QuickContainsメソッドは実際には最も遅くなります。IndexOfパフォーマンスの面でContainsはほぼ同等です。との間にToUpperこのような不一致があった理由の結果をゆがめたのは明らかに呼び出しでした。ContainsIndexOf

QuickContains拡張メソッドが最も遅くなった理由がわかりません。(おそらく属性をContains持っているという事実に関連してい[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]ますか?)。

4 オーバーロード メソッドが直接呼び出されない理由についてはまだ疑問が残りますが、(エイドリアンとデルナンがコメントで指摘したように) この決定によるパフォーマンスへの影響はないようです。

4

1 に答える 1

5

アセンブリを見てからしばらく (数年) が経ちましたが、MSIL と JIT についてはほとんど何も知らないので、いい練習になるでしょう - 抵抗できなかったので、ここに少しだけ、おそらく冗長な経験的データを示します。 . IndexOfオーバーロードはインライン化されますか?

小さなコンソール アプリを次に示します。

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

JIT は、32 ビットで実行される最適化されたリリース ビルド、任意の CPU でこれを生成します。アドレスを短縮し、無関係な行をいくつか削除しました。

--- ...\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.IndexOf4 つの引数を持つ最後のオーバーロードを直接呼び出しているようpushです。( edx: value"地獄"); this(「こんにちは」) でecx。確認するために、これはcallat 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"));
  ...
}
于 2013-07-11T20:23:55.300 に答える