.NET8 Ultimate Performance Optimization Reflection

Preface

Reflection has always been a performance bottleneck, so no matter which .NET version, the optimization of reflection is inevitable. It mainly focuses on optimization in two aspects, allocation and caching. .NET8 is no exception. Read this article.

Original text: .NET8 Ultimate Performance Optimization Reflection

Overview

For example, for the optimization of GetCustomAttributes to obtain attributes through reflection, the following example

// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0public class Tests{    public object[] GetCustomAttributes() => typeof(C).GetCustomAttributes(typeof(MyAttribute), inherit: true);
    [My(Value1 = 1, Value2 = 2)]    class C { }
    [AttributeUsage(AttributeTargets.All)]    public class MyAttribute : Attribute    {        public int Value1 { get; set; }        public int Value2 { get; set; }    }}

The obvious difference between .NET7 and .NET8 is that it is mainly optimized to avoid allocating an object[1] array to set the value of the property.

method Runtime average value ratio distribute allocation ratio
GetCustomAttributes .NET 7.0 1,287.1 ns 1.00 296 B 1.00
GetCustomAttributes .NET 8.0 994.0 ns 0.77 232 B 0.78

Others include reducing allocations on the reflection stack, such as through freer spans. Improved generic processing on Type, thereby improving the performance of various generic-related members, such as GetGenericTypeDefinition, whose results are now cached on the Type object​​​​​​

// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0public class Tests{    private readonly Type _type = typeof(List<int>);
    public Type GetGenericTypeDefinition() => _type.GetGenericTypeDefinition();}

.NET7 and .NET8 are as follows

method Runtime average value Compare
GetGenericTypeDefinition .NET 7.0 47.426 ns 1.00
GetGenericTypeDefinition .NET 8.0 3.289 ns 0.07

These are all details, and the one that affects reflection performance the most is MethodBase.Invoke. When compiling, the signature of the method is known and the method is called via reflection. You can use CreateDelegate to obtain and cache the delegate of this method, and then perform all calls through this delegate. This achieves performance optimization, but if you don't know the signature of the method at compile time, you need to rely on dynamic methods. For example, MethodBase.Invoke, this method reduces performance and is more time-consuming . Some people who are more familiar with .NET development will use emit to avoid this overhead. This method is used in .NET7. In .NET8, improvements have been made for many such situations. Previously, the emitter always generated code that could accommodate ref/out parameters, but many methods did not provide such parameters. When these factors did not need to be considered, the generated code could be more Efficient. ​​​​​​​

// If you have .NET 6 installed, you can update the csproj to include a net6.0 in the target frameworks, and then run://     dotnet run -c Release -f net6.0 --filter "*" --runtimes net6.0 net7.0 net8.0// Otherwise, you can run://     dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
using BenchmarkDotNet.Attributes;using BenchmarkDotNet.Running;using System.Reflection;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]public class Tests{    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(Tests).GetMethod("MyMethod0", BindingFlags.NonPublic | BindingFlags.Static);        _method1 = typeof(Tests).GetMethod("MyMethod1", BindingFlags.NonPublic | BindingFlags.Static);        _method2 = typeof(Tests).GetMethod("MyMethod2", BindingFlags.NonPublic | BindingFlags.Static);        _method3 = typeof(Tests).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) { }}

The situations for .NET6 and 7 and 8 are as follows:

method Runtime average value ratio
Method0 .NET 6.0 91.457 ns 1.00
Method0 .NET 7.0 7.205 ns 0.08
Method0 .NET 8.0 5.719 ns 0.06
Method1 .NET 6.0 132.832 ns 1.00
Method1 .NET 7.0 26.151 ns 0.20
Method1 .NET 8.0 21.602 ns 0.16
Method2 .NET 6.0 172.224 ns 1.00
Method2 .NET 7.0 37.937 ns 0.22
Method2 .NET 8.0 26.951 ns 0.16
Method3 .NET 6.0 211.247 ns 1.00
Method3 .NET 7.0 42.988 ns 0.20
Method3 .NET 8.0 34.112 ns 0.16

There are some issues here, there is some performance overhead involved with each call, and each call is repeated. If we can extract these repetitive tasks and cache them. You can achieve better performance. .NET8 implements these functions through the MethodInvoker and ConstructorInvoker types. These don't cover all the uncommon errors that MethodBase.Invoke handles (like specifically recognizing and handling Type.Missing), but for all other cases it provides a good workaround for optimizing repeated calls to methods whose signatures are unknown at build time plan. ​​​​​​​

// dotnet run -c Release -f net8.0 --filter "*"
using BenchmarkDotNet.Attributes;using BenchmarkDotNet.Running;using System.Reflection;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]public class Tests{    private readonly object _arg0 = 4, _arg1 = 5, _arg2 = 6;    private readonly object[] _args3 = new object[] { 4, 5, 6 };    private MethodInfo _method3;    private MethodInvoker _method3Invoker;
    [GlobalSetup]    public void Setup()    {        _method3 = typeof(Tests).GetMethod("MyMethod3", BindingFlags.NonPublic | BindingFlags.Static);        _method3Invoker = MethodInvoker.Create(_method3);    }
    [Benchmark(Baseline = true)]     public void MethodBaseInvoke() => _method3.Invoke(null, _args3);
    [Benchmark]    public void MethodInvokerInvoke() => _method3Invoker.Invoke(null, _arg0, _arg1, _arg2);
    private static void MyMethod3(int arg1, int arg2, int arg3) { }}

The situation for .NET8 is as follows

method average value ratio
MethodBaseInvoke 32.42 ns 1.00
MethodInvokerInvoke 11.47 ns 0.35

These types are used by the ActivatorUtilities.CreateFactory method in Microsoft.Extensions.DependencyInjection.Abstractions to further improve DI service build performance. This was further improved by adding an additional caching layer to further avoid reflection on every build.

 

Author:jianghupt

Welcome to follow the official account (jianghupt), where the article is first published.

IntelliJ IDEA 2023.3 & JetBrains Family Bucket annual major version update new concept "defensive programming": make yourself a stable job GitHub.com runs more than 1,200 MySQL hosts, how to seamlessly upgrade to 8.0? Stephen Chow's Web3 team will launch an independent App next month. Will Firefox be eliminated? Visual Studio Code 1.85 released, floating window US CISA recommends abandoning C/C++ to eliminate memory security vulnerabilities Yu Chengdong: Huawei will launch disruptive products next year and rewrite industry history TIOBE December: C# is expected to become the programming language of the year A paper written by Lei Jun 30 years ago : "Principle and Design of Computer Virus Determination Expert System"
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/5407571/blog/10320411