Reflection Invoke performance improvements .NET 6 v .NET 7 v .NET 8 benchmarks




Date Added (UTC):

15 Apr 2024 @ 18:53

Date Updated (UTC):

15 Apr 2024 @ 18:53


.NET Version(s):

.NET 6 .NET 7 .NET 8

Tag(s):

#.Net7PerfImprovement #.Net8PerfImprovement #Reflection


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using System.Reflection;

[HideColumns("Error", "StdDev", "Median", "RatioSD")]
[MemoryDiagnoser(displayGenColumns: false)]
[Config(typeof(Config))]
public class ReflectionMethodInvoke
{
    private MethodInfo _method0, _method1, _method2, _method3;
    private readonly object[] _args1 = new object[] { 1 };
    private readonly object[] _args2 = new object[] { 2, 3 };
    private readonly object[] _args3 = new object[] { 4, 5, 6 };

    [GlobalSetup]
    public void Setup()
    {
        _method0 = typeof(ReflectionMethodInvoke).GetMethod("MyMethod0", BindingFlags.NonPublic | BindingFlags.Static);
        _method1 = typeof(ReflectionMethodInvoke).GetMethod("MyMethod1", BindingFlags.NonPublic | BindingFlags.Static);
        _method2 = typeof(ReflectionMethodInvoke).GetMethod("MyMethod2", BindingFlags.NonPublic | BindingFlags.Static);
        _method3 = typeof(ReflectionMethodInvoke).GetMethod("MyMethod3", BindingFlags.NonPublic | BindingFlags.Static);
    }

    [Benchmark] public void Method0() => _method0.Invoke(null, null);
    [Benchmark] public void Method1() => _method1.Invoke(null, _args1);
    [Benchmark] public void Method2() => _method2.Invoke(null, _args2);
    [Benchmark] public void Method3() => _method3.Invoke(null, _args3);

    private static void MyMethod0() { }
    private static void MyMethod1(int arg1) { }
    private static void MyMethod2(int arg1, int arg2) { }
    private static void MyMethod3(int arg1, int arg2, int arg3) { }

    private class Config : ManualConfig
    {
        public Config()
        {
            AddJob(Job.Default.WithId(".NET 6").WithRuntime(CoreRuntime.Core60).AsBaseline());
            AddJob(Job.Default.WithId(".NET 7").WithRuntime(CoreRuntime.Core70));
            AddJob(Job.Default.WithId(".NET 8").WithRuntime(CoreRuntime.Core80));

            SummaryStyle =
                SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
        }
    }
}

// .NET 6, .NET 7, .NET 8
public void Method0()
{
    _method0.Invoke(null, null);
}
// .NET 6, .NET 7, .NET 8
public void Method1()
{
    _method1.Invoke(null, _args1);
}
// .NET 6, .NET 7, .NET 8
public void Method2()
{
    _method2.Invoke(null, _args2);
}
// .NET 6, .NET 7, .NET 8
public void Method3()
{
    _method3.Invoke(null, _args3);
}

// .NET 6
.method public hidebysig 
    instance void Method0 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 27 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x210d
    // Code size 15 (0xf)
    .maxstack 8

    // sequence point: (line 39, col 42) to (line 39, col 69) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method0
    IL_0006: ldnull
    IL_0007: ldnull
    IL_0008: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_000d: pop
    IL_000e: ret
}

// .NET 7
.method public hidebysig 
    instance void Method0 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 27 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x211d
    // Code size 15 (0xf)
    .maxstack 8

    // sequence point: (line 39, col 42) to (line 39, col 69) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method0
    IL_0006: ldnull
    IL_0007: ldnull
    IL_0008: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_000d: pop
    IL_000e: ret
}

// .NET 8
.method public hidebysig 
    instance void Method0 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 27 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20cd
    // Code size 15 (0xf)
    .maxstack 8

    // sequence point: (line 39, col 42) to (line 39, col 69) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method0
    IL_0006: ldnull
    IL_0007: ldnull
    IL_0008: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_000d: pop
    IL_000e: ret
}
// .NET 6
.method public hidebysig 
    instance void Method1 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 28 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x211d
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 40, col 42) to (line 40, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method1
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args1
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}

// .NET 7
.method public hidebysig 
    instance void Method1 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 28 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x212d
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 40, col 42) to (line 40, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method1
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args1
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}

// .NET 8
.method public hidebysig 
    instance void Method1 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 28 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20dd
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 40, col 42) to (line 40, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method1
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args1
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}
// .NET 6
.method public hidebysig 
    instance void Method2 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 29 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2132
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 41, col 42) to (line 41, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method2
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args2
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}

// .NET 7
.method public hidebysig 
    instance void Method2 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 29 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2142
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 41, col 42) to (line 41, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method2
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args2
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}

// .NET 8
.method public hidebysig 
    instance void Method2 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 29 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20f2
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 41, col 42) to (line 41, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method2
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args2
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}
// .NET 6
.method public hidebysig 
    instance void Method3 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2147
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 42, col 42) to (line 42, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method3
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args3
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}

// .NET 7
.method public hidebysig 
    instance void Method3 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2157
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 42, col 42) to (line 42, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method3
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args3
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}

// .NET 8
.method public hidebysig 
    instance void Method3 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2107
    // Code size 20 (0x14)
    .maxstack 8

    // sequence point: (line 42, col 42) to (line 42, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Reflection.MethodInfo ReflectionMethodInvoke::_method3
    IL_0006: ldnull
    IL_0007: ldarg.0
    IL_0008: ldfld object[] ReflectionMethodInvoke::_args3
    IL_000d: callvirt instance object [System.Runtime]System.Reflection.MethodBase::Invoke(object, object[])
    IL_0012: pop
    IL_0013: ret
}

// .NET 6 (X64), .NET 7 (X64)
Method0()
    L0000: sub rsp, 0x38
    L0004: mov rcx, [rcx+8]
    L0008: xor edx, edx
    L000a: mov [rsp+0x20], rdx
    L000f: mov [rsp+0x28], rdx
    L0014: xor r8d, r8d
    L0017: xor r9d, r9d
    L001a: mov rax, [rcx]
    L001d: mov rax, [rax+0x58]
    L0021: call qword ptr [rax+0x38]
    L0024: nop
    L0025: add rsp, 0x38
    L0029: ret
// .NET 8 (X64)
Method0()
    L0000: sub rsp, 0x38
    L0004: mov rcx, [rcx+8]
    L0008: xor edx, edx
    L000a: mov [rsp+0x20], rdx
    L000f: mov [rsp+0x28], rdx
    L0014: xor r8d, r8d
    L0017: xor r9d, r9d
    L001a: mov rax, [rcx]
    L001d: mov rax, [rax+0x58]
    L0021: call qword ptr [rax+0x30]
    L0024: nop
    L0025: add rsp, 0x38
    L0029: ret
// .NET 6 (X64), .NET 7 (X64)
Method1()
    L0000: sub rsp, 0x38
    L0004: mov rax, [rcx+0x10]
    L0008: mov rcx, [rcx+0x28]
    L000c: mov [rsp+0x20], rcx
    L0011: xor ecx, ecx
    L0013: mov [rsp+0x28], rcx
    L0018: mov rcx, rax
    L001b: xor edx, edx
    L001d: xor r8d, r8d
    L0020: xor r9d, r9d
    L0023: mov rax, [rax]
    L0026: mov rax, [rax+0x58]
    L002a: call qword ptr [rax+0x38]
    L002d: nop
    L002e: add rsp, 0x38
    L0032: ret
// .NET 8 (X64)
Method1()
    L0000: sub rsp, 0x38
    L0004: mov rax, [rcx+0x10]
    L0008: mov rcx, [rcx+0x28]
    L000c: mov [rsp+0x20], rcx
    L0011: xor ecx, ecx
    L0013: mov [rsp+0x28], rcx
    L0018: mov rcx, rax
    L001b: xor edx, edx
    L001d: xor r8d, r8d
    L0020: xor r9d, r9d
    L0023: mov rax, [rax]
    L0026: mov rax, [rax+0x58]
    L002a: call qword ptr [rax+0x30]
    L002d: nop
    L002e: add rsp, 0x38
    L0032: ret
// .NET 6 (X64), .NET 7 (X64)
Method2()
    L0000: sub rsp, 0x38
    L0004: mov rax, [rcx+0x18]
    L0008: mov rcx, [rcx+0x30]
    L000c: mov [rsp+0x20], rcx
    L0011: xor ecx, ecx
    L0013: mov [rsp+0x28], rcx
    L0018: mov rcx, rax
    L001b: xor edx, edx
    L001d: xor r8d, r8d
    L0020: xor r9d, r9d
    L0023: mov rax, [rax]
    L0026: mov rax, [rax+0x58]
    L002a: call qword ptr [rax+0x38]
    L002d: nop
    L002e: add rsp, 0x38
    L0032: ret
// .NET 8 (X64)
Method2()
    L0000: sub rsp, 0x38
    L0004: mov rax, [rcx+0x18]
    L0008: mov rcx, [rcx+0x30]
    L000c: mov [rsp+0x20], rcx
    L0011: xor ecx, ecx
    L0013: mov [rsp+0x28], rcx
    L0018: mov rcx, rax
    L001b: xor edx, edx
    L001d: xor r8d, r8d
    L0020: xor r9d, r9d
    L0023: mov rax, [rax]
    L0026: mov rax, [rax+0x58]
    L002a: call qword ptr [rax+0x30]
    L002d: nop
    L002e: add rsp, 0x38
    L0032: ret
// .NET 6 (X64), .NET 7 (X64)
Method3()
    L0000: sub rsp, 0x38
    L0004: mov rax, [rcx+0x20]
    L0008: mov rcx, [rcx+0x38]
    L000c: mov [rsp+0x20], rcx
    L0011: xor ecx, ecx
    L0013: mov [rsp+0x28], rcx
    L0018: mov rcx, rax
    L001b: xor edx, edx
    L001d: xor r8d, r8d
    L0020: xor r9d, r9d
    L0023: mov rax, [rax]
    L0026: mov rax, [rax+0x58]
    L002a: call qword ptr [rax+0x38]
    L002d: nop
    L002e: add rsp, 0x38
    L0032: ret
// .NET 8 (X64)
Method3()
    L0000: sub rsp, 0x38
    L0004: mov rax, [rcx+0x20]
    L0008: mov rcx, [rcx+0x38]
    L000c: mov [rsp+0x20], rcx
    L0011: xor ecx, ecx
    L0013: mov [rsp+0x28], rcx
    L0018: mov rcx, rax
    L001b: xor edx, edx
    L001d: xor r8d, r8d
    L0020: xor r9d, r9d
    L0023: mov rax, [rax]
    L0026: mov rax, [rax+0x58]
    L002a: call qword ptr [rax+0x30]
    L002d: nop
    L002e: add rsp, 0x38
    L0032: ret


Benchmark Description:


[PR](https://github.com/dotnet/runtime/pull/88415) .NET 8 saw further improvements to add to the major ones in .NET 7. From Stephen Toub Performance Improvement in .NET 8 blog post ... _However, the largest impact on performance in reflection in .NET 8 comes from dotnet/runtime#88415. This is a continuation of work done in .NET 7 to improve the performance of MethodBase.Invoke. When you know at compile-time the signature of the target method you want to invoke via reflection, you can achieve the best performance by using CreateDelegate<DelegateType> to get and cache a delegate for the method in question, and then performing all invocations via that delegate. However, if you don’t know the signature at compile-time, you need to rely on more dynamic means, like MethodBase.Invoke, which historically has been much more costly. Some enterprising developers turned to reflection emit to avoid that overhead by emitting custom invocation stubs at run-time, and that’s one of the optimization approaches taken under the covers in .NET 7 as well. Now in .NET 8, the code generated for many of these cases has improved; previously the emitter was always generating code that could accommodate ref/out arguments, but many methods don’t have such arguments, and the generated code can be more efficient when it needn’t factor those in._

This benchmark setup is designed to measure the performance of method invocations using reflection in .NET. Reflection is a powerful feature that allows runtime inspection of assemblies, types, and members, and the ability to dynamically invoke methods. However, it's known to introduce performance overhead compared to direct method calls, primarily due to the additional work required to resolve method metadata and ensure type safety at runtime. The benchmarks aim to quantify this overhead under different conditions. ### General Setup - **.NET Versions:** The benchmarks are configured to run against multiple .NET versions: .NET 6, .NET 7, and a hypothetical .NET 8, allowing for performance comparison across these versions. This can help in identifying performance improvements or regressions introduced in newer versions of the .NET runtime. - **BenchmarkDotNet:** A powerful library for benchmarking .NET code, providing detailed performance metrics. The configuration suppresses columns like Error, StdDev, Median, and RatioSD for clarity, focusing on the mean execution time and memory usage. - **MemoryDiagnoser:** Enabled without displaying garbage collection generation columns, focusing on the total memory allocated by each benchmark method. ### Benchmark Methods Rationale 1. **Method0 (No Arguments):** Measures the overhead of invoking a method with no parameters using reflection. This is the simplest case and serves as a baseline for understanding the minimum overhead introduced by reflection-based invocation. 2. **Method1 (One Argument):** Tests the performance impact of invoking a method with a single parameter. This helps in understanding how passing arguments affects the reflection invocation overhead, particularly the cost associated with packing arguments into an object array and the potential impact on the garbage collector due to array allocations. 3. **Method2 (Two Arguments):** Further explores the impact of method complexity on reflection invocation performance by increasing the number of parameters. This can provide insights into how linear or non-linear the performance degradation is with an increasing number of method arguments. 4. **Method3 (Three Arguments):** Represents an even more complex method invocation scenario with three parameters. This benchmark aims to capture the performance characteristics of invoking methods with multiple parameters, which is common in real-world applications, and assesses the scalability of reflection-based method calls as the argument count grows. ### Insights and Expected Results - **Performance Overhead:** All benchmarks are expected to highlight the overhead of using reflection for method invocation compared to direct calls, with increasing costs as the number of arguments increases due to the additional work required to handle argument arrays. - **Memory Allocation:** Memory diagnostics will reveal the allocations associated with argument array handling. Methods with more arguments are expected to allocate more memory due to the need for larger arrays. - **.NET Version Comparison:** By running the benchmarks across different .NET versions, one can identify runtime optimizations over time. For example, if newer .NET versions have improved the performance of reflection or reduced the associated memory allocations, this would be visible in the benchmark results. - **Importance:** Understanding the performance characteristics of reflection-based method invocations is crucial for developers who rely on dynamic features in performance-critical applications. It helps in making informed decisions about when and how to use reflection, balancing flexibility and performance. In summary, these benchmarks are designed to provide a comprehensive view of the performance implications of using reflection for method invocations in .NET, across different scenarios and runtime versions.


Benchmark Comments: