当前位置: 首页 > 科技观察

为什么.NET的反射这么慢?

时间:2023-03-12 16:27:06 科技观察

大家都知道.NET中的反射很慢,但是为什么会这样呢?本文将带领大家找到这个问题的真正原因。CLR类型系统的设计目标的原因之一是反射本身并不是为高性能而设计的。可以参考TypeSystemOverview-'DesignGoalsandNon-goals'(TypeSystemOverview-'DesignGoalsandNon-goals'target'):目标运行时通过快速执行(非反射)代码访问需要的信息.编译时直接获取生成代码所需的信息。垃圾收集/遍历堆栈可以访问所需的信息,而无需锁定或分配内存。一次只加载最少量的类型。在类型加载期间只加载最少需要的类型。类型系统的数据结构必须保存在NGEN映像中。除了对象元数据之外的所有信息都可以直接反映CLR数据结构。快速使用反射。参考TypeLoaderDesign-'KeyDataStructures'来自同一来源:EEClassMethodTable(方法表)数据分为“热”和“冷”结构,以提高工作集和缓存利用率。MethodTable本身只存储程序稳定状态的“热”数据。EEClass存储“冷”数据,这些数据通常是类型加载、JITing或反射所需要的。每个MethodTable指向一个EEClass。反射如何工作?我们已经知道反射本身并不是为了快而设计的,但是为什么要花那么多时间呢?为了说明这个问题,我们来看看托管代码和非托管代码在反射调用时的调用栈。System.Reflection.RuntimeMethodInfo.Invoke(..)-源链接调用System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..)System.RuntimeMethodHandle.PerformSecurityCheck(..)-链接调用System.GC.KeepAlive(..)System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..)-链接调用System.RuntimeMethodHandle.InvokeMethod(..)的stub能够直观的感受到方法执行了很多代码。参考例子:System.RuntimeMethodHandle.InvokeMethodis400多行代码!那么,它到底在做什么呢?获取方法信息要使用反射调用字段/属性/方法,您必须获取FieldInfo/PropertyInfo/MethodInfo,使用如下代码:Typet=typeof(Person);FieldInfom=t.GetField("姓名");这需要一定的成本,因为需要提取和解析相关的元数据。运行时将帮助我们维护一个内部缓存,缓存所有字段/属性/方法。此缓存由RuntimeTypeCache类实现,使用示例在RuntimeMethodInfo类中。运行要点中的代码,您可以看到缓存是如何工作的,它恰当地使用反射来检查运行时内部!要点上的代码将在使用反射获取FieldInfo之前输出以下内容:该字段,它将输出:Type:ReflectionOverhead.ProgramReflectionType:System.RuntimeType(BaseType:System.Reflection.TypeInfo)RuntimeTypeCache:System.RuntimeType+RuntimeTypeCache,m_cacheComplete=True,4itemsincache[0]-Int32TestField1-Private[1]-System.StringTestField2-Private[2]-Int32k__BackingField-Private[3]-System.StringTestField3-Private,StaticReflectionOverhead.Program看起来像这样:classProgram{privateintTestField1;私有字符串TestField2;私有静态字符串TestField3;私有intTestProperty1{得到;放;}}看起来运行时会过滤已经创建的东西,这意味着调用GetField或GetFields不会花费太多。GetMethod和GetProperty也是如此,MethodInfo或PropertyInfo将在您第一次调用时创建并缓存。参数校验和错误处理拿到MethodInfo之后,如果调用它的Invoke方法,需要处理很多事情。假设代码是这样写的:PropertyInfostringLengthField=typeof(string).GetProperty("Length",BindingFlags.Instance|BindingFlags.Public);varlength=stringLengthField.GetGetMethod().Invoke(newUri(),newobject[0]);如果你运行上面的代码,你会得到以下异常:System.Reflection.TargetException:Objectdoesnotmatchtargettype。在System.Reflection.RuntimeMethodInfo.CheckConsistency(..)在System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(..)因为我们拿到了String类的Length属性的PropertyInfo,却在Uri对象上调用了,显然,这是一个错误的类型!此外,在调用方法时,还必须验证传递给方法的参数。为了传递参数,反射API使用一个对象数组作为参数,其中每个元素代表一个参数。因此,如果使用反射调用Add(intx,inty)方法,则必须调用methodInfo.Invoke(..,new[]{5,6})。将在运行时检查传入参数的数量和类型。本例中需要保证有2个int类型的参数。这些工作的缺点是通常需要装箱,这会增加额外的成本。希望将来这会归结为***。安全检查另一个主要任务是多重安全检查。例如,您不允许使用反射来调用您想要的任何方法。有一些受限或“危险的方法”只能从受信任的.NETFramework代码中调用。除了黑名单之外,还有动态安全检查,由调用时必须检查的当前代码访问安全权限决定。反射机制需要多少时间?了解了反射的实际操作之后,我们再来看看实际耗时。请注意,这些基准测试是通过反射直接比较读/写属性完成的。在.NET中,属性是一对Get/Set方法,由编译器生成,但是当属性仅包含一个简单的内联字段时,.NETJIT使用内联Get/Set方法来提高性能。这意味着使用反射访问属性可能具有最差的反射性能,但选择它是因为这是最常见的用例,数据在ORM、Json序列化/反序列化库和对象映射工具中。下面是BenchmarkDotNet提供的原始结果,后面是两个单独表格中显示的相同结果。(在这里下载所有基准代码)读取属性值('Get')写入属性值('Set')我们可以清楚地看到普通反射代码(GetViaReflection/SetViaReflection)比直接访问属性(GetViaProperty/SetViaProperty)慢得多。我们需要进一步分析其他结果。设置首先我们从一个TestClass开始,代码如下:publicclassTestClass{publicTestClass(Stringdata){Data=data;}私有字符串数据;私有字符串数据{获取{返回数据;}设置{数据=值;}}}和下面的通用代码,此处包含所有可用选项://设置代码,仅完成一次TestClasstestClass=newTestClass("AString");Type@class=testClass.GetType();BindingFlagbindingFlags=BindingFlags实例|绑定标志.NonPublic|BindingFlags.Public;正常反射首先,我们使用常规基准代码来表示我们的初始情况和“最坏情况”:return(string)property.GetValue(testClass,null);}选项1-缓存PropertyInfo接下来,我们通过保存对PropertyInfo的引用而不是每次获取它来获得小幅速度提升。但即便如此,它仍然比直接访问属性慢得多,这意味着反射的“调用”部分的成本很高。//设置代码,只完成一次PropertyInfocachedPropertyInfo=@class.GetProperty("Data",bindingFlags);[Benchmark]publicstringGetViaReflection(){return(string)cachedPropertyInfo.GetValue(testClass,null);}选项2-使用FastMember这里使用了MarcGravell优秀的FastMember库,非常好用!//设置代码,只做一次TypeAccessoraccessor=TypeAccessor.Create(@class,allowNonPublicAccessors:true);[Benchmark]publicstringGetViaFastMember(){return(string)accessor[testClass,"Data"];}注意其他选项略有不同,它创建一个TypeAccessor来访问类型中的所有属性,而不仅仅是一个。这样做的缺点是运行时间更长,因为它首先在内部为您请求的属性(本例中为“数据”)创建一个委托,然后获取它的值。虽然这个开销很小,但FastMember仍然比其他反射方法更快、更容易使用。所以我建议你先检查一下。这次选择和后面的选择把反射代码转成delegates,这样每次都可以直接调用,不用反射,所以提高了速度!必须指出的是,创建委托需要一定的成本(在“相关阅读”中阅读更多相关信息)。简而言之,速度的提升是因为我们对它进行了很大的投入(安全检查等),省下了一个强类型的delegate,然后我们就可以用很小的投入来调用它。如果反射只进行一次,则不需要使用这些技术。但是如果只做一次反射操作,就不会成为性能瓶颈,根本不在乎会不会慢!通过委托读取属性仍然不如直接访问它快,因为.NETJIT不会内联优化对委托方法的调用,而直接访问属性则可以。所以即使有委托,我们也需要付出调用方法的代价,而直接访问属性则不需要。选项3-创建代理(Delegate)在此选项中,我们使用CreateDelegate函数将PropertyInfo转换为常规委托://设置代码,仅完成一次PropertyInfoproperty=@class.GetProperty("Data",bindingFlags);FuncgetDelegate=(Func)Delegate.CreateDelegate(typeof(Func),property.GetGetMethod(nonPublic:true));[Benchmark]publicstringGetViaDelegate(){返回getDelegate(testClass);}它的缺点是编译时必须知道具体的类型,也就是上面代码中的Func部分(如果使用Func,编译器会抛出异常!).不过,在大多数情况下,使用反射不会遇到太多麻烦。为避免麻烦,请参阅MagicMethodHelper代码(在JonSkeet的“让反射飞起来并探索委托”博客中),或阅读下面的选项4或5。选项4——编译表达式树这里我们生成了一个委托,但不同的是我们可以传入一个对象,所以我们看到了“选项4”的局限性。我们使用支持动态代码生成的.NETExpression树API://Setupcode,doneonlyoncePropertyInfoproperty=@class.GetProperty("Data",bindingFlags);ParameterExpression=Expression.Parameter(typeof(object),"instance");UnaryExpressioninstanceCast=!property.DeclaringType.IsValueType?Expression.TypeAs(instance,property.DeclaringType):Expression.Convert(instance,property.DeclaringType);FuncGetDelegate=Expression.Lambda>(Expression.TypeAs(Expression.Call(instanceCast,property.GetGetMethod(nonPublic:true)),typeof(object)),instance).Compile();[Benchmark]publicstringGetViaCompiledExpressionTrees(){return(string)GetDelegate(testClass);}所有关于Expression的代码都可以从“使用表达式树进行更快的反射”博客下载。方案5——ILEmit动态代码生成***,虽然“能力越大责任越大”,但这里我们还是使用顶层方法调用原来的IL,://设置代码,只做一次PropertyInfoproperty=@class.GetProperty("Data",bindingFlags);Sigil.EmitgetterEmiter=Emit>.NewDynamicMethod("GetTestClassDataProperty").LoadArgument(0).CastClass(@class).Call(property.GetGetMethod(nonPublic:true)).Return();Funcgetter=getterEmiter.CreateDelegate();[Benchmark]publicstringGetViaILEmit(){returngetter(testClass);}使用表达式树(如选项4中所述),并没有像直接调用IL代码那样给你很大的灵活性,尽管它确实可以防止你调用无效代码!考虑到这一点,如果您发现自己真的需要emitIL,我强烈建议您使用出色的Sigil库,因为它会在出现问题时提供更好的错误消息!总结如果(且仅当)你发现自己在使用反射时遇到性能问题,有一些方法可以让它更快。这些速度提升的实现是因为委托带来了对属性/字段/方法的直接访问,这避免了每次都进行反射的开销。请参考/r/programming和/r/csharp讨论本文