Почему 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 перегрузки не вызывается напрямую, но кажется, что производительность не влияет (как указал Адриан и Делнан в комментариях) решением.
Ответы
Ответ 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"));
...
}