Start Debugging

Производительность .NET 8: UnsafeAccessor против рефлексии

Бенчмарк UnsafeAccessor против рефлексии в .NET 8. Посмотрите, как UnsafeAccessor добивается производительности без накладных расходов по сравнению с классической рефлексией.

В предыдущей статье мы рассмотрели, как обращаться к приватным членам с помощью UnsafeAccessor. На этот раз мы хотим взглянуть на его производительность по сравнению с рефлексией и понять, действительно ли это решение без накладных расходов.

Мы проведём четыре бенчмарка.

  1. Reflection: измеряем получение приватного метода из типа и его вызов.
  2. Reflection с кешем: похоже на предыдущий вариант, но вместо того чтобы получать метод каждый раз, используем закешированную ссылку на MethodInfo.
  3. Unsafe accessor: вызов того же приватного метода с помощью UnsafeAccessor вместо рефлексии.
  4. Прямой доступ: непосредственный вызов публичного метода. Это будет ориентиром, по которому мы поймём, действительно ли UnsafeAccessor обеспечивает производительность без накладных расходов.

Если вы хотите запустить бенчмарки сами, ниже приведён код:

[SimpleJob(RuntimeMoniker.Net80)]
public class Benchmarks
{
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "PrivateMethod")]
    extern static int PrivateMethod(Foo @this, int value);

    static readonly Foo _instance = new();

    static readonly MethodInfo _privateMethod = typeof(Foo)
        .GetMethod("PrivateMethod", BindingFlags.Instance | BindingFlags.NonPublic);

    [Benchmark]
    public int Reflection() => (int)typeof(Foo)
        .GetMethod("PrivateMethod", BindingFlags.Instance | BindingFlags.NonPublic)
        .Invoke(_instance, [42]);

    [Benchmark]
    public int ReflectionWithCache() => (int)_privateMethod.Invoke(_instance, [42]);

    [Benchmark]
    public int UnsafeAccessor() => PrivateMethod(_instance, 42);

    [Benchmark]
    public int DirectAccess() => _instance.PublicMethod(42);
}

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

| Method              | Mean       | Error     | StdDev    |
|-------------------- |-----------:|----------:|----------:|
| Reflection          | 35.9979 ns | 0.1670 ns | 0.1562 ns |
| ReflectionWithCache | 21.2821 ns | 0.2283 ns | 0.2135 ns |
| UnsafeAccessor      |  0.0035 ns | 0.0022 ns | 0.0018 ns |
| DirectAccess        |  0.0028 ns | 0.0024 ns | 0.0023 ns |

Результаты весьма впечатляют. Если сравнивать прямой доступ и unsafe accessor, разницы буквально нет. Те несколько наносекунд разницы между ними можно отбросить как шум: на самом деле, если запустить бенчмарки несколько раз, иногда unsafe accessors даже оказываются быстрее. Это совершенно нормально и по сути говорит нам, что оба варианта эквивалентны, то есть без накладных расходов.

Сравнивать UnsafeAccessor с рефлексией почти не имеет смысла. По производительности накладных расходов нет, и в качестве бонуса вы получаете весь синтаксический сахар, связанный с настоящей сигнатурой метода.

Это не значит, что рефлексия мертва. UnsafeAccessor покрывает только сценарии, в которых тип и член, к которому нужно обратиться, известны на этапе компиляции. Если эта информация доступна только во время выполнения, рефлексия по-прежнему остаётся правильным выбором.

Код бенчмарков также доступен на GitHub.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад