【.NET8】访问私有成员新姿势UnsafeAccessor(下)
前言
书接上回,我们讨论了在.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做的实现。
我们目前只关注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这里主要是支持UnsafeAccessor
和static extern int
声明函数的用法,需要支持方法的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
它直接就是一个static extern int GetValueUnsafe(A a);
方法,没有中间开销,而且它IL Body很小,可以被内联。
总结
通过对.NET8中新增的UnsafeAccessor
特性的深入探究,我们得到了一些启示和理解。首先,UnsafeAccessor
的引入并非无中生有,而是应运而生,它是为了满足System.Text.Json或EF Core这类框架在访问私有成员时的需求,因为它们目前大多基于Emit动态代码生成实现,但在AOT环境中无法使用Emit,只能依赖于效率较低的反射API。因此,UnsafeAccessor
的引入,为我们提供了一种零开销的私有成员访问机制。
总的来说,UnsafeAccessor
的引入无疑为.NET的发展增添了一抹亮色,它不仅提升了代码的执行效率,也为我们的编程方式提供了新的可能。我们期待在未来的.NET版本中,看到更多这样的创新和突破。
【.NET8】访问私有成员新姿势UnsafeAccessor(下)的更多相关文章
- C#中访问私有成员
首先访问一个类的私有成员不是什么好做法.大家都知道私有成员在外部是不能被访问的.一个类中会存在很多私有成员:如私有字段.私有属性.私有方法.对于私有成员造访,可以套用下面这种非常好的方式去解决. pr ...
- C#中访问私有成员技巧
源代码是别人的,你就不能修改源代码,只提供给你dll.或者你去维护别人的代码,源代码却有丢失.这样的情况如果你想知道私有成员的值,甚至去想直接调用类里面的私有方法.那怎么办呢?其实在.net中访问私有 ...
- C#中访问私有成员--反射
首先我必须承认访问一个类的私有成员不是什么好做法.大家也都知道私有成员在外部是不能被访问的.而一个类中会存在很多私有成员:如私有字段.私有属性.私有方法.对于私有成员访问,可以套用下面这种非常好的方式 ...
- CPP-基础:关于私有成员的访问
a.C++的类的成员函数中,允许直接访问该类的对象的私有成员变量. b.在类的成员函数中可以访问同类型实例的私有变量. c.拷贝构造函数里,可以直接访问另外一个同类对象(引用)的私有成员. d.类的成 ...
- VC6.0中友元函数无法访问类私有成员的解决办法
举个例子: 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #inclu ...
- 使用C#反射机制访问类的私有成员【转】
首先我必须承认访问一个类的私有成员不是什么好做法.大家也都知道私有成员在外部是不能被访问的.而一个类中会存在很多私有成员:如私有字段.私有属性.私有方法.对于私有成员访问,可以套用下面这种非常好的方式 ...
- java中用反射访问私有方法和私有成员[转]
转自: http://zhouyangchenrui.iteye.com/blog/470521 java的反射可以绕过访问权限,访问到类的私有方法和成员.可能这点会引起安全性的讨论.反射的使用帮助解 ...
- JS 的私有成员为什么钦定了 #?
翻译自 tc39/proposal-class-fields 译者按:社区一直以来有一个声音,就是反对使用 # 声明私有成员.但是很多质疑的声音过于浅薄.人云亦云.其实 TC39 早就对此类呼声做过回 ...
- [置顶] c++,vc6.0,中友元函数,无法访问私有字段(private)的问题(problem),cannot access private member declared in class 'Date'
c++,vc6.0,中友元函数,无法访问私有字段(private)的问题(problem),cannot access private member declared in class 'Date' ...
- [C++参考]私有成员变量的理解
私有成员变量的概念,在脑海中的现象是,以private关键字声明,是类的实现部分,不对外公开,不能在对象外部访问对象的私有成员变量. 然而,在实现拷贝构造函数和赋值符函数时,在函数里利用对象直接访问了 ...
随机推荐
- 「AntV」X6开发实践:踩过的坑与解决方案
长期更新版文档请移步语雀(「AntV」X6开发实践:踩过的坑与解决方案 (yuque.com)) ️ | 如何自定义拖拽源? 相信你们在开发中更多的需求是需要自定义拖拽源,毕竟自定义的功能扩展性高一些 ...
- 洛谷 P7579 「RdOI R2」称重(weigh) 题解
题意: 题目 一道交互题. 有 n 个球,里面有两个假球,假球比普通球的要轻,每次可以询问任意两组球的轻重关系,第一组轻为 < ,第二组轻为 > ,一样重量为 = . 思路: 先考虑在一个 ...
- 函数接口(Functional Interfaces)
定义 首先,我们先看看函数接口在<Java语言规范>中是怎么定义的: 函数接口是一种只有一个抽象方法(除Object中的方法之外)的接口,因此代表一种单一函数契约.函数接口的抽象方法可以是 ...
- Zabbix Timeout 设置不当导致的问题
哈喽大家好,我是咸鱼 今天跟大家分享一个关于 zabbix Timeout 值设置不当导致的问题,这个问题不知道大家有没有碰到过 问题 事情经过是这样的: 把某一台 zabbix agent 的模板由 ...
- GGTalk 开源即时通讯系统源码剖析之:服务端全局缓存
继上篇<GGTalk 开源即时通讯系统源码剖析之:数据库设计>介绍了 GGTalk 数据库中所有表的结构后,接下来我们将进入GGTalk服务端的核心部分. GGTalk 对需要频繁查询数据 ...
- 配置k8s拉取Harbor镜像
创建Secret # 认证名称为:docker-harbor-registry kubectl create secret docker-registry docker-harbor-registry ...
- 我不知道的threejs(6)-开发中的容易被忽略的
在threejs Editor中调好一些样式属性后, 可以直接选择导出具体的格式,或者导出成json[json 一般体积大很多,比glb](场景,通过objectLoader 加载json!!!) 自 ...
- Istio 入门(五):访问控制和流量管理
本教程已加入 Istio 系列:https://istio.whuanle.cn 目录 4, 流量管理 基于版本的路由配置 基于 Http header 的路由配置 故障注入 两种故障注入 比例分配流 ...
- C语言基础--逻辑判断和循环
目录 一.储存标识符 1.auto 2.register 3.static 4.const 二.运算符 1.逻辑运算符 2.位运算符 3.运算符 4.三元运算符 三.选择结构 1.if判断 1.1 i ...
- MASABlazor在移动端点击保持出现悬停样式
提出问题 最近在学习MAUIBlazor,用的MASA Blazor,发现在移动端(触屏设备)上,点击会一直显示悬停样式,如下图所示,简单研究了一下,发现这是移动端的通病. 解决问题 MASABlaz ...