Почему String.Contains не вызывает окончательную перегрузку напрямую? - программирование

Почему String.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 для пояснения скорости)

Чтобы уточнить различия в производительности, которые я получаю, если я где-то ошибся: Я провел этот тест (зациклился 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 кажется на 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.Ordinal в вызове 2 перегрузки IndexOf и удалите ToUpper, метод QuickContains фактически станет самым медленным. IndexOf и Contains в значительной степени соответствуют производительности. Так ясно, что вызов ToUpper исказил результаты того, почему было такое несоответствие между Contains и IndexOf.

Не знаю, почему метод расширения QuickContains стал самым медленным. (Возможно, связано с тем, что Contains имеет атрибут [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]?).

Вопрос по-прежнему заключается в том, почему метод 4 перегрузки не вызывается напрямую, но кажется, что производительность не влияет (как указал Адриан и Делнан в комментариях) решением.

4b9b3361

Ответ 1

Прошло некоторое время (годы), так как я посмотрел на сборку, и я почти ничего не знаю о MSIL и JIT, так что это было бы неплохое упражнение - не могло устоять, так что здесь немного, возможно, избыточно, экспериментальные данные. Перегружается ли перегрузка IndexOf?

Здесь крошечное консольное приложение:

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

JIT генерирует это в оптимизированной версии релиза, любой процессор, работающий в 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]

Здесь call на 0x00000025:

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; один в edx (value: "ад" ); this ( "hello" ) в ecx. Чтобы подтвердить это, здесь находится 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"));
  ...
}