.NET8极致性能优化Reflection

点击上方蓝字 江湖评谈关注我

b7609cb0f526e09327f18b51a05ea164.png

前言

反射一直是性能的瓶颈,所以无论哪个.NET版本反射的优化必然少不了。主要是集中在两个方面优化,分配和缓存。.NET8自然也不例外。本篇看下。

概述

比如针对GetCustomAttributes 通过反射获取属性的优化,以下例子

// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
public 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; }
    }
}

.NET7和.NET8明显的差异,它主要是优化了避免分配一个object[1]数组来设置属性的值

方法运行时平均值比率分配分配比率
GetCustomAttributes.NET 7.01,287.1 ns1.00296 B1.00
GetCustomAttributes.NET 8.0994.0 ns0.77232 B0.78

其它的比如减少反射堆栈中的分配,比如通过更自由的spans。改进了Type上的泛型处理,从而提升各种与泛型相关的成员性能,比如GetGenericTypeDefinition,它的结果现在被缓存在了Type对象上

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


    public Type GetGenericTypeDefinition() => _type.GetGenericTypeDefinition();
}

.NET7和.NET8如下

方法运行时平均值
GetGenericTypeDefinition.NET 7.047.426 ns1.00
GetGenericTypeDefinition.NET 8.03.289 ns0.07

这些都是细枝末节,影响反射性能最大的一块是MethodBase.Invoke。当在编译的时候,知道方法的签名并且通过反射来调用方法。就可以通过使用CreateDelegate来获取和缓存该方法的委托,然后通过该委托执行所有的调用。从而实现性能最佳化,但是如果在编译的时候你不知道方法的签名,则需要依赖动态的方法。比如MethodBase.Invoke,这个方法降低性能并且更耗时。一些比较了解.NET开发的人员会用 emit避免这种开销。.NET7里面采用这种方式。.NET8里面,为许多这样的情况进行了改进,以前,emitter 总是生成可以容纳 ref/out 参数的代码,但许多方法不提供这样的参数,当不需要考虑这些因素时,生成的代码可以更高效。

// 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) { }
}

.NET6以及7和8的情况分别如下:

方法运行时平均值比率
Method0.NET 6.091.457 ns1.00
Method0.NET 7.07.205 ns0.08
Method0.NET 8.05.719 ns0.06
Method1.NET 6.0132.832 ns1.00
Method1.NET 7.026.151 ns0.20
Method1.NET 8.021.602 ns0.16
Method2.NET 6.0172.224 ns1.00
Method2.NET 7.037.937 ns0.22
Method2.NET 8.026.951 ns0.16
Method3.NET 6.0211.247 ns1.00
Method3.NET 7.042.988 ns0.20
Method3.NET 8.034.112 ns0.16

这里有一些问题,每次调用都会涉及到一些性能开销,每次调用都会重复。如果我们可以提取这些重复性的工作,对它们进行缓存。就可以实现更好的性能。.NET8里面通过 MethodInvoker 和 ConstructorInvoker 类型中实现了这些功能。这些并没有包含所有 MethodBase.Invoke 处理的不常见错误(如特别识别和处理 Type.Missing),但对于其他所有情况,它为优化在构建时未知签名的方法的重复调用提供了一个很好的解决方案。

// 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) { }
}

.NET8的情况如下

方法平均值比率
MethodBaseInvoke32.42 ns1.00
MethodInvokerInvoke11.47 ns0.35

这些类型被 Microsoft.Extensions.DependencyInjection.Abstractions 中的 ActivatorUtilities.CreateFactory 方法使用,以进一步提高 DI 服务构建性能。通过添加额外的缓存层进一步改进,进一步避免每次构建时的反射。e10841c82186d600f6c73e63079c5c45.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值