Почему typeA == typeB медленнее, чем typeA == typeof (TypeB)?

Недавно я оптимизировал/протестировал некоторый код и наткнулся на этот метод:

public void SomeMethod(Type messageType)
{
    if (messageType == typeof(BroadcastMessage))
    {
        // ...
    }
    else if (messageType == typeof(DirectMessage))
    {
        // ...
    }
    else if (messageType == typeof(ClientListRequest))
    {
        // ...
    }
}

Это вызывается из цикла, критичного к производительности, в другом месте, поэтому я естественно предположил, что все эти вызовы typeof(...) добавляли ненужные накладные расходы (я знаю, что это микрооптимизация) и могли быть перемещены в частные поля в классе. (Я знаю, что есть лучшие способы реорганизовать этот код, однако я все еще хотел бы знать, что здесь происходит.)

Согласно моему тесту, это совсем не так (используется BenchmarkDotNet).

[DisassemblyDiagnoser(printAsm: true, printSource: true)]
[RyuJitX64Job]
public class Tests
{
    private Type a = typeof(string);
    private Type b = typeof(int);

    [Benchmark]
    public bool F1()
    {
        return a == typeof(int);
    }

    [Benchmark]
    public bool F2()
    {
        return a == b;
    }
}

Результаты на моей машине (Window 10 x64,.NET 4.7.2, RyuJIT, Release build):

Функции, скомпилированные до ASM:

F1

mov     rcx,offset mscorlib_ni+0x729e10
call    clr!InstallCustomModule+0x2320
mov     rcx,qword ptr [rsp+30h]
cmp     qword ptr [rcx+8],rax
sete    al
movzx   eax,al

F2

mov     qword ptr [rsp+30h],rcx
mov     rcx,qword ptr [rcx+8]
mov     rdx,qword ptr [rsp+30h]
mov     rdx,qword ptr [rdx+10h]
call    System.Type.op_Equality(System.Type, System.Type)
movzx   eax,al

Я не знаю, как интерпретировать ASM, поэтому не могу понять значение происходящего здесь. В скорлупе ореха почему F1 быстрее?

Ответы

Ответ 1

Размещенная вами сборка показывает, что комментарий mjwills, как и ожидалось, правильный. Как отмечает связанная статья, джиттер может быть осторожен в определенных сравнениях, и это одно из них.

Давайте посмотрим на ваш первый фрагмент:

mov     rcx,offset mscorlib_ni+0x729e10

rcx - это указатель this при вызове функции-члена. "Этот указатель" в этом случае будет адресом предварительно выделенного объекта CLR, чего точно я не знаю.

call    clr!InstallCustomModule+0x2320

Теперь мы вызываем некоторую функцию-член для этого объекта; Я не знаю что. Ближайшая общедоступная функция, для которой у вас есть информация отладки, - InstallCustomModule, но, очевидно, мы не вызываем InstallCustomModule здесь; мы вызываем функцию на расстоянии 0x2320 байт от InstallCustomModule.

Было бы интересно посмотреть, что делает код в InstallCustomModule + 0x2320.

В любом случае, мы делаем вызов, и возвращаемое значение идет в rax. Двигаясь дальше:

mov     rcx,qword ptr [rsp+30h]
cmp     qword ptr [rcx+8],rax

Это похоже, что это извлечение значения a отказом от this и сравнивая его с любой функцией возвращается.

Остальная часть кода просто совершенно обычная: перемещение результата bool сравнения в регистр возврата.

Короче говоря, первый фрагмент эквивалентен:

return ReferenceEquals(SomeConstantObject.SomeUnknownFunction(), this.a);

Очевидно, что обоснованное предположение здесь состоит в том, что постоянный объект и неизвестная функция являются специальными помощниками, которые быстро выбирают часто используемые объекты типа, такие как typeof (int).

Второе обоснованное предположение состоит в том, что джиттер сам для себя решает, что шаблон "сравнить поле типа Type с typeof (что-то)" лучше всего можно сделать в виде прямого эталонного сравнения между объектами.

И теперь вы можете сами убедиться, что делает второй фрагмент. Просто:

return Type.op_Equality(this.a, this.b);

Все, что он делает, - это вызывает вспомогательный метод, который сравнивает два типа на равенство значений. Помните, что CLR не гарантирует равенство ссылок для всех объектов эквивалентного типа.

Теперь должно быть понятно, почему первый фрагмент быстрее. Джиттер знает гораздо больше о первом фрагменте. Он знает, например, что typeof (int) всегда будет возвращать одну и ту же ссылку, и поэтому вы можете сделать дешевое сравнение ссылок. Он знает, что typeof (int) никогда не бывает нулевым. Он знает точный тип typeof (int) - помните, Type не запечатан; Вы можете создавать свои собственные объекты Type.

Во втором фрагменте джиттер не знает ничего, кроме того, что у него есть два операнда типа Type. Он не знает их типов во время выполнения, он не знает их недействительности; Насколько он знает, вы сами подклассифицировали Type и создали два экземпляра, которые являются неравнозначными, но равными по значению. Он должен вернуться к наиболее консервативной позиции и вызвать вспомогательный метод, который начинает идти вниз по списку: оба они равны нулю? Является ли одно из нулевого, а другой ненулевым? равны ли они? И так далее.

Похоже, отсутствие знаний обходится вам в огромные штрафы... полсекунды. Я бы не беспокоился об этом.

Ответ 2

Если вам интересно, вы также можете взглянуть на логику, которую использует jit, см. GtFoldTypeCompare.

Jit может сделать целый ряд вещей, чтобы упростить или даже устранить сравнение типов. Все они требуют знания чего-то о создании сравниваемых типов.