前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【.NET8】访问私有成员新姿势UnsafeAccessor(下)

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

作者头像
InCerry
发布2023-09-21 16:15:27
3060
发布2023-09-21 16:15:27
举报
文章被收录于专栏:InCerryInCerry

前言

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

测试代码

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

代码语言:javascript
复制

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做的实现。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

为什么性能这么好?

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

总结

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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-20 08:46,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 InCerry 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 测试代码
  • 深入探究
    • 新增功能的原因
      • 如何做到访问私有成员?
        • 为什么性能这么好?
        • 总结
        相关产品与服务
        腾讯云服务器利旧
        云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档