首页 > 其他分享 >【.NET8】访问私有成员新姿势UnsafeAccessor(下)

【.NET8】访问私有成员新姿势UnsafeAccessor(下)

时间:2023-09-20 09:02:40浏览次数:42  
标签:私有 UnsafeAccessor public static NET8 var TestInstance Emit

前言

书接上回,我们讨论了在.NET8中新增的UnsafeAccessor,并且通过UnsafeAccessor访问了私有成员,这极大的方便了我们代码的编写,当然也聊到了它当前存在的一些局限性,那么它的性能到底如何?我们今天就来实际测试一下。

测试代码

话不多说,直接上代码,本次测试代码如下:

using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using Perfolizer.Horology;

[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class AccessBenchmarks
{
    public static readonly A TestInstance = new();
    public static readonly Action<A, int> SetDelegate;
    public static readonly Func<A, int> GetDelegate;
    public static readonly PropertyInfo ValueProperty;
    public static readonly MethodInfo SetValueMethod;
    public static readonly MethodInfo GetValueMethod;

    public static readonly Func<A, int> GetValueExpressionFunc;
    public static readonly Action<A, int> SetValueExpressionAction;

    static AccessBenchmarks()
    {
        TestInstance = new();
        ValueProperty = typeof(A).GetProperty("Value");
        SetValueMethod = ValueProperty.GetSetMethod();
        GetValueMethod = ValueProperty.GetGetMethod();

        SetDelegate = CreateSetDelegate();
        GetDelegate = CreateGetDelegate();

        GetValueExpressionFunc = CreateGetValueExpressionFunc();
        SetValueExpressionAction = CreateSetValueExpressionAction();
    }

    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Value")]
    static extern int GetValueUnsafe(A a);

    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Value")]
    static extern void SetValueUnsafe(A a, int value);

    [Benchmark]
    public void UnsafeAccessor()
    {
        SetValueUnsafe(TestInstance, 10);
        var value = GetValueUnsafe(TestInstance);
    }

    [Benchmark]
    public void Reflection()
    {
        SetValueMethod.Invoke(TestInstance, new object[] { 10 });
        var value = GetValueMethod.Invoke(TestInstance, new object[] { });
    }

    [Benchmark]
    public void Emit()
    {
        SetDelegate(TestInstance, 10);
        var value = GetDelegate(TestInstance);
    }

    [Benchmark]
    public void ExpressionTrees()
    {
        SetValueExpressionAction(TestInstance, 10);
        var value = GetValueExpressionFunc(TestInstance);
    }

    [Benchmark]
    public void Direct()
    {
        TestInstance.Value = 10;
        var value = TestInstance.Value;
    }

    private static Action<A, int> CreateSetDelegate()
    {
        var dynamicMethod = new DynamicMethod("SetValue", null, new[] { typeof(A), typeof(int) }, typeof(A));
        var ilGenerator = dynamicMethod.GetILGenerator();
        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.Emit(OpCodes.Ldarg_1);
        ilGenerator.EmitCall(OpCodes.Call, SetValueMethod, null);
        ilGenerator.Emit(OpCodes.Ret);
        return (Action<A, int>)dynamicMethod.CreateDelegate(typeof(Action<A, int>));
    }

    private static Func<A, int> CreateGetDelegate()
    {
        var dynamicMethod = new DynamicMethod("GetValue", typeof(int), new[] { typeof(A) }, typeof(A));
        var ilGenerator = dynamicMethod.GetILGenerator();
        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.EmitCall(OpCodes.Call, GetValueMethod, null);
        ilGenerator.Emit(OpCodes.Ret);
        return (Func<A, int>)dynamicMethod.CreateDelegate(typeof(Func<A, int>));
    }

    private static Func<A, int> CreateGetValueExpressionFunc()
    {
        var instance = Expression.Parameter(typeof(A), "instance");
        var getValueExpression = Expression.Lambda<Func<A, int>>(
            Expression.Property(instance, ValueProperty),
            instance);

        return getValueExpression.Compile();
    }

    private static Action<A, int> CreateSetValueExpressionAction()
    {
        var instance = Expression.Parameter(typeof(A), "instance");
        var value = Expression.Parameter(typeof(int), "value");
        var setValueExpression = Expression.Lambda<Action<A, int>>(
            Expression.Call(instance, ValueProperty.GetSetMethod(true), value),
            instance, value);

        return setValueExpression.Compile();
    }
}

public class A
{
    public int Value { get; set; }
}

public class Program
{
    public static void Main()
    {
        Console.WriteLine(AccessBenchmarks.TestInstance);
        var summary = BenchmarkRunner.Run<AccessBenchmarks>(DefaultConfig.Instance.WithSummaryStyle(new SummaryStyle(
            cultureInfo: null, // use default
            printUnitsInHeader: true,
            printUnitsInContent: true,
            sizeUnit: SizeUnit.B,
            timeUnit: TimeUnit.Nanosecond,
            printZeroValuesInContent: true,
            ratioStyle: RatioStyle.Trend // this will print the ratio column
        )));
    }
}

在测试代码中,我们使用了BenchmarkDotNet来进行测试,测试的内容包括:

  • UnsafeAccessor:使用UnsafeAccessor特性来访问私有成员
  • Reflection:使用反射访问私有成员
  • Emit:使用Emit+动态方法访问私有成员
  • ExpressionTrees:使用表达式树+委托来访问私有成员
  • Direct:直接访问私有成员

测试结果如下图所示,可以看到使用UnsafeAccessor的性能是最好的,其次是直接访问私有成员,最差的是反射。这其实是出乎我的意料的,因为我认为它最多和直接访问私有成员的性能差不多,但是实际上它的性能比直接访问私有成员还要好,当然也有可能是统计的误差,0.0000ns这个尺度已经非常小了。

深入探究

看到这里我想大家都有很多疑问,实际上作者本人看到这里也是有很多的疑问,主要是这两个:

  • 是什么原因让.NET社区想加入这个API?
  • 它是如何做到访问私有成员的?
  • 为什么性能会这么好?

新增功能的原因

如果要了解这个功能背后的东西,那么我们首先就要找到对应这个API的Issues,按照.NET社区的规范,所有的API都需要提交Issues,然后经过API Review,多轮讨论设计以后,才会开始开发。

首先我们定位到Issue是这一个,在Issue中我们可以了解到这个API主要是为了给System.Text.Json或EF Core这种需要访问私有成员的框架使用,因为目前它们都是基于Emit动态代码生成实现的,但是Emit不能在AOT中使用,现阶段只能使用慢速的反射API,所以迫切引入了一种零开销的私有成员访问机制。

https://github.com/dotnet/runtime/issues/86161

如何做到访问私有成员?

翻阅一下整个API提案Issue的讨论,我们可以找到具体实现的Issue,所以我们要了解它背后的原理的话,就需要跳转到对应的Issue。

在这里可以看到目前还没有做泛型的实现,非泛型的已经在下面链接中实现了,一个是为CoreCLR做的实现,另外一个是为Mono做的实现。

image-20230919223206177

我们目前只关注CoreCLR,点开这个Issue。

https://github.com/dotnet/runtime/issues/86161

image-20230919223551807

可以看到将这个任务拆成了几个部分,他们都在在一个PR中完成的,其中包括定义了UnsafeAccessor特性,在JIT中的实现,以及NativeAOT中进行了支持,另外编写了单元测试加入了有效的诊断方案。

那么来看看这个PR里面做了什么吧。

https://github.com/dotnet/runtime/pull/86932

由于PR非常的长,大家有兴趣可以点进去看看,低于8GB内存的小伙伴就要小心了。简单的来说这次修改主要就是两块地方,一块是JIT相关的修改,JIT这里主要是支持UnsafeAccessorstatic extern int声明函数的用法,需要支持方法的IL Body为空,然后在JIT时根据特性为它插入代码。

首先我们来看JIT的处理,这块代码主要就是修改了jitinterface.cpp,可以看到它调用了TryGenerateUnsafeAccessor方法:

image-20230919225057007

这个TryGenerateUnsafeAccessor方法实现在prestub.cpp中,这个prestub.cpp实现了一些预插桩的操作,TryGenerateUnsafeAccessor方法实现如下所示:

image-20230919225441624

它针对UnsafeAccessorKind的不同枚举做了校验,防止出现运行时崩溃的情况:

image-20230919225638593

然后调用了GenerateAccessor方法来生成IL:

image-20230919225739682

GenerateAccessor里面就是使用Emit进行代码生成:

image-20230919225932113

所以从JIT的实现来看,它其实核心原理就是Emit代码生成,并没有太多特殊的东西。

另外是关于NativeAOT的实现,首先修改了NativeAotILProvider.cs这个类,这个类的主要作用就是在进行NativeAot的时候提供IL给JIT预先编译使用:

image-20230919230445386

关键也是在GenerateAccessor方法里面,在这里生成了对应的IL代码:

image-20230919230733590

总结一下,UnsafeAccessor实现原理还是使用的IL动态生成技术,只不过它是在JIT内部实现的。

为什么性能这么好?

那么它为什么性能要比我们在C#代码中自己写Emit要更好呢?其实原因也是显而易见的,我们自己编写的Emit代码中间有一层DynamicMethod的委托调用,增加了开销,而UnsafeAccessor它直接就是一个static extern int GetValueUnsafe(A a);方法,没有中间开销,而且它IL Body很小,可以被内联。

总结

通过对.NET8中新增的UnsafeAccessor特性的深入探究,我们得到了一些启示和理解。首先,UnsafeAccessor的引入并非无中生有,而是应运而生,它是为了满足System.Text.Json或EF Core这类框架在访问私有成员时的需求,因为它们目前大多基于Emit动态代码生成实现,但在AOT环境中无法使用Emit,只能依赖于效率较低的反射API。因此,UnsafeAccessor的引入,为我们提供了一种零开销的私有成员访问机制。

总的来说,UnsafeAccessor的引入无疑为.NET的发展增添了一抹亮色,它不仅提升了代码的执行效率,也为我们的编程方式提供了新的可能。我们期待在未来的.NET版本中,看到更多这样的创新和突破。

标签:私有,UnsafeAccessor,public,static,NET8,var,TestInstance,Emit
From: https://www.cnblogs.com/InCerry/p/dotnet8-access-private-member-new-method-2.html

相关文章

  • 私有化部署企业即时通讯(企业im)除了钉钉还有这些
    在现代企业中,私有化部署企业即时通讯平台已经成为确保数据安全和实现高效通信的重要手段。除了众所周知的钉钉,WorkPlus作为领先品牌,提供私有化部署企业即时通讯的领先选择。本文将介绍WorkPlus为企业提供的广阔领域和精彩特点,展示除了钉钉外更多潜力等待您去发现。一、灵活定制,适应......
  • 使用ztncui配置私有化zerotier服务器
    众所周知,Zerotier-One是一个非常好的组建虚拟局域网的工具,可以以p2p的方式穿透NAT网络进行连接。但是在使用中也仍然存在着一些瑕疵,主要就是以下两点:因为Zerotier官方提供的中心节点(planet节点)全部都在国外,所以更新路由表的时候,通常会消耗较长的时间,甚至可能失败;官方免费版......
  • 即时通讯私有化部署,为什么更符合企业对钉钉和企微的替代需求?
    随着企业对安全性和数据隐私保护的关注日益增加,私有化部署已成为替代钉钉和企业微信的趋势。WorkPlus作为领先品牌,致力于提供私有化部署的解决方案,以满足企业对即时通讯和协作的需求。本文将深入探讨为何私有化部署更符合企业的要求,使WorkPlus成为钉钉和企业微信的理想替代方案。一......
  • 【.NET8】访问私有成员新姿势UnsafeAccessor(上)
    前言前几天在.NET性能优化群里面,有群友聊到了.NET8新增的一个特性,这个类叫UnsafeAccessor,有很多群友都不知道这个特性是干嘛的,所以我就想写一篇文章来带大家了解一下这个特性。其实在很早之前我就有关注到这个特殊的特性,但是当时.NET8还没有正式发布,所以我也没有写文章,现在.NET8......
  • YApi 私有化部署(Win)
    YApi官方文档介绍了可视化部署和命令行部署两种私有化部署方式,下面在官方文档的基础上介绍第二种部署方法。环境准备要求有Node(7.6+)、MongoDB(2.6+)、Git。这里只介绍MongoDB的安装。到download/community下载MongoDB,我下载的是7.0.1版。安装MongoDB(见如何在Wind......
  • harbor-私有镜像仓库的离线安装部署
    harbor-私有镜像仓库的离线安装部署最低安装条件:资源最低限度推荐CPU2核4核内存4GB8GB最低软件要求:软件版本描述Dockerengine版本17.06.0-ce+或更高版本有关安装说明,请参阅Docker引擎文档DockerCompose版本1.18.0或更高版本有......
  • 为什么企业需要视频会议私有部署?
    随着全球化和数字化的快速发展,企业必须不断适应新的沟通方式,以满足不断变化的市场需求。互联网技术的普及使得远程办公成为可能,这意味着员工分散在不同的地理位置,需要一种高效的方式来进行协作和沟通。此外,全球供应链的日益复杂也使企业需要与合作伙伴和客户保持紧密联系,以确保生产......
  • .Net8 AOT+VMP简单的逆向分析
    1.前言测试下VMP加密.NET的强度,选了最新的.Net8+AOT编译,用VMP给它加壳。最后逆向下,简单的分析,本篇看下。2.概述一.前奏首先一段简单的C#代码:namespaceTest_{internalclassProgram{staticvoidMain(string[]args){Console.WriteLine("hello,......
  • 关于Flutter的webview无法访问私有SSL证书链接的问题
    优先考虑用 Freessl 技术方案: 问题原因项目部署服务器的Https为私有SSL证书,目前确认不会提供共有SSL证书,导致WebView访问网页空白问题官方设计WebView插件时,为了考虑安全性在访问https链接时,必须要求SSL证书有效,不支持自定义SSL错误的处理解决方式有三种处理方式:使......
  • 白嫖一个属于你的私有大模型
    最近国内的大模型可谓是遍地开花,你瞧瞧:这么火,我也想搞一个试试,于是就有了这篇文章!对,你没看错,就是白嫖。毕竟人家清华都开源了,哈哈哈hoho~~先把开源地址贴一下,老铁们可以自行去瞧一瞧:https://github.com/THUDM/ChatGLM-6Bhttps://huggingface.co/THUDM/chatglm-6bChatGLM-......