.NET 8 Performance: UnsafeAccessor vs. Reflection
Benchmarking UnsafeAccessor vs Reflection in .NET 8. See how UnsafeAccessor achieves zero-overhead performance compared to traditional reflection.
In a previous article we covered how to access private members using UnsafeAccessor. This time around, we want to look at its performance compared to Reflection, and to see whether it’s truly zero-overhead or not.
We’re going to do four benchmarks.
- Reflection: we benchmark retrieving a private method from a type and invoking it.
- Reflection with cache: similar to the one above, but instead of retrieving the method each time, we use a cached reference to the
MethodInfo. - Unsafe accessor: calling the same private method using
UnsafeAccessorinstead of reflection. - Direct access: calling a public method directly. This should serve as a benchmark to see if
UnsafeAccessortruly provides zero-overhead performance.
If you want to run the benchmarks yourself, you have the code below:
[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);
}
Benchmark results
| 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 |
The results are quite impressive. Comparing direct access to unsafe accessor, there’s literally no difference. The few nanoseconds difference between the two can be discarded as noise – in fact, if you run the benchmarks a few times, you might even get instances where unsafe accessors are faster. That’s perfectly normal, and it’s basically telling us that the two are equivalent – thus zero-overhead.
There’s almost no point in comparing UnsafeAccessor to reflection. Performance-wise you have no overhead, and as a bonus you also get all the sugar that comes with having an actual method signature.
That’s not to say that reflection is dead. UnsafeAccessor only covers scenarios where you know the type and member that needs to be accessed at compile-time. If that information is only available to you at runtime, reflection is still the way to go.
Benchmarks code is also available on GitHub.