Найти в Дзене

Ускоряем вызов метода через System.Reflection

В разработке приложений на C# иногда возникает необходимость вызsвать методы, поступающие к нам в виде MethodInfo. Самым простым способом это осуществить является вызов метода MethodInfo.Invoke(object? obj, object?[]? parameters), но так ли он быстр?
Давайте рассмотрим этот вопрос и найдем все альтернативы этому способу. Для этого мы напишем небольшой бенчмарк, используя библиотеку BenchmarkDotNet. В замерах мы будем учитывать вызов метода 1 раз и 1000 раз.

1) MethodInfo.Invoke какой он есть.

Код бенчмарка
Код бенчмарка

В замерах на 1 вызов он выполняется за 29 ns.
В замерах на 1000 вызовов он выполняется за 27,600 ns.
Этот код выделяет 88 байт при вызове, где по 16 байт уходит на boxing структур и остальное на инициализацию массива.

2) Вызов через Delegate

Код бенчмарка
Код бенчмарка

В замерах на 1 вызов он выполняется за 280 ns .
В замерах на 1000 вызовов он выполняется за 2,360 ns.
Данный вариант создаст единожды 64 байт на аллокацию делегата.
Этот вариант довольно эффективен среди всех "safe" способов, если будет закеширован для дальнейших вызовов.

3) Вызов через указатель на функция

Код бенчмарка
Код бенчмарка

В замерах на 1 вызов он выполняется за 18 ns .
В замерах на 1000 вызовов он выполняется за 1,622 ns.
Этот код не выделяет никакой памяти.
По скорости почти как вызвать этот метод напрямую, но увы, некоторые не особо любят "unsafe".

4) Вызов MethodInfo.Invoke.
Идеальный компромисс между "unsafe" и "safe". (исходный код ObjectParamsArray и Box<T> будет в конце статьи)

Код бенчмарка
Код бенчмарка

В замерах на 1 вызов он выполняется за 20 ns .
В замерах на 1000 вызовов он выполняется за 18,823 ns.
Этот код не выделяет никакой памяти.
Этот вариант второй по скорости на 1 вызов и 3 по скорости на 1000 вызовов.
Вся магия происходит за счет мимикризации структур под соответствующим им объекты. В .NET boxed структуры имеют следующий вид: [ 0x0...0x8 - указатель на MethodTable объекта; 0x8...(0x8 + sizeof(Структуры) ]. Соответственно, чтобы выдать нашу структуру за boxed структуру, нам надо поместить в начало структуры указатель на нее! Само собой надо нашей псевдо-структуре добавить атрибут StructLayout(Sequential), чтобы jit компилятор не утащил указатель на MethodTable куда-нибудь в конец.

Пример обертки, чтобы выдать структуру за boxed
Пример обертки, чтобы выдать структуру за boxed

Остается дело за малым, превратить указатель на структуру в object. В .NET 8 это достигается обычным Unsafe.BitCast указателя в object.

Общие результаты опыта:

Результаты бенчмарка
Результаты бенчмарка

Ссылка на код бенчмарка: https://gist.github.com/BadRyuner/602a94b1f3b4e054258dbec165f24d68

Всем спасибо за внимание!