大家都知道 .NET 的反射很慢 ,但是为什么会出现这种情况呢?这篇文章会带你寻找这个问题的真正原因。
首先需要指出的一部分原因是,在设计的时候反射本身就不是以 高性能 为目标的,可以参考 Type System Overview - ‘Design Goals and Non-goal s’ (类型系统概览 - ‘设计目标和非目标’):
编译时直接访问所需要的信息来生成代码。
垃圾回收/遍历栈可以访问需要信息而不需要锁或分配内存。
一次只加载最少量的类型。
类型加载时只加载最少需要加载的类型。
类型系统的数据结构必须在 NGEN 映像中保存。
元数据的所有信息能直接反射 CLR 数据结构。
参阅出处相同的 Type Loader Design - ‘Key Data Structures’(类型加载器设计 - ‘关键数据结构’) :
MethodTable(方法表)数据分为“热”和“冷”两种结构,以提高工作集和缓存的利用率。MethodTable 本身只存储程序稳定状态的“热”数据。 EEClass 存储“冷”数据,它们通常是类型加载、JITing或反射所需要的。 每个 MethodTable 指向一个 EEClass。
边城
翻译于 6天前
1 人顶
翻译得不错哦!
为了说明这个问题,来看看反射调用过程中,托管代码和非托管代码的调用栈。
System.Reflection.RuntimeMethodInfo.Invoke(..) - 源码链接
调用 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal (..)
System.RuntimeMethodHandle.PerformSecurityCheck(..) - 链接
调用 System.GC.KeepAlive (..)
System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..) - 链接
调用 System.RuntimeMethodHandle.InvokeMethod (..) 的存根
System.RuntimeMethodHandle.InvokeMethod(..) 的存根 - 链接
即使你不点击链接去看那些 C#/cpp 方法,你也能直观地了感受到这个方法过程中执行了 不少 代码。不过给你一个例子,最后那个方法,做了其中大部分的工作, System.RuntimeMethodHandle.InvokeMethodis 超过 400 行代码 !
边城
翻译于 6天前
0 人顶
翻译得不错哦!
这是一个很好的概括,但是它 具体 在做什么?
要使用反射来调用字段/属性/方法,你必须获得 FieldInfo/PropertyInfo/MethodInfo,使用这样的代码:
Type t = typeof(Person); FieldInfo m = t.GetField("Name");
这需要一定的成本,因为需要提取相关的元数据,并对其进行解析等。有趣的是运行时会帮我们维持一个内部缓存,缓存着所有这些字段/属性/方法。这个缓存由 RuntimeTypeCache 类 实现,用法上有一个示例在 RuntimeMethodInfo 类 中.
运行 这个 gist 中的代码你可以看到缓存是如何运作的,它恰如其分地使用反射检查运行时内部!
gist 上 的代码会在你使用反射获得 FieldInfo 之前打印下面的东西:
Type: ReflectionOverhead.Program Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo) m_fieldInfoCache is null, cache has not been initialised yet
不过一旦你获得了哪怕一个字段,就会打印:
Type: ReflectionOverhead.Program Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo) RuntimeTypeCache: System.RuntimeType+RuntimeTypeCache, m_cacheComplete = True, 4 items in cache [0] - Int32 TestField1 - Private [1] - System.String TestField2 - Private [2] - Int32 <TestProperty1>k__BackingField - Private [3] - System.String TestField3 - Private, Static
ReflectionOverhead.Program 看起来像这样:
class Program { private int TestField1; private string TestField2; private static string TestField3; private int TestProperty1 { get; set; } }
看来运行时会筛选已经创建过的东西,这意味着调用 GetFeild 或 GetFields 不需要多大代价。对于 GetMethod 和 GetProperty 来说也是如此,MethodInfo 或 PropertyInfo 会在你第一次调用的时候创建并缓存起来。
边城
翻译于 6天前
0 人顶
翻译得不错哦!
得到 MethodInfo 之后,如果你调用它的 Invoke 方法,会发现它还要处理很多事项。假设编写代码如下:
PropertyInfo stringLengthField = typeof(string).GetProperty("Length", BindingFlags.Instance | BindingFlags.Public); var length = stringLengthField.GetGetMethod().Invoke(new Uri(), new object[0]);
如果运行上述代码,会得到下面的异常:
System.Reflection.TargetException: Object does not match target type. at System.Reflection.RuntimeMethodInfo.CheckConsistency(..) at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(..) at System.Reflection.RuntimeMethodInfo.Invoke(..) at System.Reflection.RuntimePropertyInfo.GetValue(..)
这是因为我们获得了 String 类 Length 属性的 PropertyInfo,但是却在 Uri 对象上调用它,显然,这是个错误的类型!
此外,你还必须在调用方法时对传递给方法的参数进行校验。为了能传递参数,反射 API 使用了一个 object 的数组作为参数,其中每一个元素表示一个参数。所以,如果你使用反射来调用 Add(int x, int y) 方法,你得调用 methodInfo.Invoke(.., new [] { 5, 6 })。运行时会对传入参数的数量和类型进行检查,在这个示例中你要确保是 2 个 int 类型的参数。这些工作不好的地方是常常需要 装箱 ,这会增加额外的成本。希望这在 将来会降到最低 。
Viyi
翻译于 5天前
0 人顶
翻译得不错哦!
同时发生的另一个主要任务是多重安全性检查。例如,你不允许使用反射来任意调用你想调用的方法。这里存在一些限制的或 ‘危险方法’ ,只能由可信度高的 .NET 框架代码调用。除了黑名单外,还有动态安全检查,它由 调用时必须检查的 的当前 代码访问安全权限 决定。
Tocy
翻译于 6天前
0 人顶
翻译得不错哦!
现在我们已经知道反射在幕后实际做了些什么,是时候来看看我们实际的耗时了。请注意,这些基准测试是通过反射直接比较读/写属性来完成的。在 .NET 中属性实际上是一对 Get/Set 方法,这是由编译器为我们生成的,但当属性只包含一个简单的内嵌字段时,.NET JIT 会使用内联 Get/Set 方法以提升性能。这意味着使用反射访问属性可能会遇到反射性能最差的情况,但它会被选择是因为这是最常见的用例,数据位于 ORMs , Json 序列化/反序列化库 和 对象映射工具 中。
以下是由 BenchmarkDotNet 提供的原始结果,后面是在2个单独的表中显示的相同结果。 (全部 Benchmark 代码由此下载 )
Tocy
翻译于 6天前
0 人顶
翻译得不错哦!
我们可以清楚地看到,正常的反射代码(GetViaReflection/SetViaReflection)比直接访问属性(GetViaProperty/SetViaProperty)要慢得多。 但是,对于其他结果,我们还要更详细地分析下。
首先我们从 aTestClass 开始,代码类似下边:
public class TestClass { public TestClass(String data) { Data = data; } private string data; private string Data { get { return data; } set { data = value; } } }
以及下面的通用代码,这里包含了所有可用的选项:
// Setup code, done only once TestClass testClass = new TestClass("A String"); Type @class = testClass.GetType(); BindingFlag bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
首先我们使用常规基准代码来表示我们的起始情况和“最坏情况”:
[Benchmark]public string GetViaReflection() { PropertyInfo property = @class.GetProperty("Data", bindingFlags); return (string)property.GetValue(testClass, null); }
Tocy
翻译于 6天前
0 人顶
翻译得不错哦!
接下来,我们通过保存引用至 PropertyInfo 以获得速度上的少量提升,而不是每次都去获取。但即使这样,与直接访问属性相比,也仍然慢得多,这就表明在反射的“调用”部分成本很高。
// Setup code, done only once PropertyInfo cachedPropertyInfo = @class.GetProperty("Data", bindingFlags); [Benchmark] public string GetViaReflection() { return (string)cachedPropertyInfo.GetValue(testClass, null); }
这里使用了 Marc Gravell 优秀的 Fast Member 库 ,这个库用起来很简单!
// Setup code, done only once TypeAccessor accessor = TypeAccessor.Create(@class, allowNonPublicAccessors: true); [Benchmark] public string GetViaFastMember() { return (string)accessor[testClass, "Data"]; }
注意,与其他选择稍有不同,它创建了一个 TypeAccessor 来访问类型中的 所有 属性,而不仅是某一个。这带来的负面影响是会导致运行时间变长,因为它在内部首先要为你请求的属性 (这个例子中是‘Data’) 创建委托,然后再获取其值。不过这种开销是很小的,FastMember 仍然比其它反射方法更快,也更易用。所以我建议你先去看看。
这个选择及随后的选择将反射代码转换 委托 ,这样就可以直接调用而不再需要每次都进行反射,速度因此得到提升!
必须指出创建一个委托需要一定的成本(可以从 ‘相关阅读’ 了解更多)。总之,速度提升是因为我们在其中进行过一次大投入(安全检查等)并保存了一个强类型的委托,之后我们只要稍微付出一点就可以一次次调用。如果反射只进行一次,那你大可不必使用这些技术。但是如果你只进行一次反射操作,它也不会出现性能瓶颈,你就完全不用在乎它会变慢!
通过委托读某个属性仍然不如直接访问来得快,因为 .NET JIT 不会将对委托方法的调用进行内联优化,而直接访问属性则会。因此即使使用委托,我们也需要为调用方法付出成本,而直接访问属性就不会。
Viyi
翻译于 5天前
0 人顶
翻译得不错哦!
在这个选项中,我们使用 CreateDelegate 函数来将 PropertyInfo 转换为常规的 delegate:
// Setup code, done only once PropertyInfo property = @class.GetProperty("Data", bindingFlags); Func<TestClass, string> getDelegate = (Func<TestClass, string>)Delegate.CreateDelegate( typeof(Func<TestClass, string>), property.GetGetMethod(nonPublic: true)); [Benchmark] public string GetViaDelegate() { return getDelegate(testClass); }
它的缺点是你必须知道编译时的具体类型,也就是上面的代码中的 Func<TestClass,string> 部分(没错,你不能使用 Func<object,string>,如果你这样做了,编译器会抛出一个异常!)。在大多数情况下,使用反射不会遇到这么多麻烦,否则你就不会优先考虑使用反射。而此处是一个不完整的解决方案。
有一个非常有趣/费脑筋的方式可以绕过这一点,请参阅 MagicMethodHelper 代码(在 Jon Skeet 发布的“ Making Reflection fly and exploring delegates “博客中),或阅读下面的选项 4 或 5。
Tocy
翻译于 6天前
0 人顶
翻译得不错哦!
这里我们生成一个 delegate,但不同的是我们可以传入一个 object,所以我们会看到“选项4”的限制。我们使用支持动态代码生成的 .NET Expression tree API :
// Setup code, done only once PropertyInfo property = @class.GetProperty("Data", bindingFlags); ParameterExpression = Expression.Parameter(typeof(object), "instance"); UnaryExpression instanceCast = !property.DeclaringType.IsValueType ? Expression.TypeAs(instance, property.DeclaringType) : Expression.Convert(instance, property.DeclaringType); Func<object, object> GetDelegate = Expression.Lambda<Func<object, object>>( Expression.TypeAs( Expression.Call(instanceCast, property.GetGetMethod(nonPublic: true)), typeof(object)), instance) .Compile(); [Benchmark] public string GetViaCompiledExpressionTrees() { return (string)GetDelegate(testClass); }
关于 Expression 的全部代码可以从“ Faster Reflection using Expression Trees (使用表达式树的快速反射机制)“博客下载。
Tocy
翻译于 6天前
0 人顶
翻译得不错哦!
最后,虽然“权力越大,责任越大”,但这里我们还是使用最底层的方法调用原始 IL,:
// Setup code, done only once PropertyInfo property = @class.GetProperty("Data", bindingFlags); Sigil.Emit getterEmiter = Emit<Func<object, string>> .NewDynamicMethod("GetTestClassDataProperty") .LoadArgument(0) .CastClass(@class) .Call(property.GetGetMethod(nonPublic: true)) .Return(); Func<object, string> getter = getterEmiter.CreateDelegate(); [Benchmark] public string GetViaILEmit() { return getter(testClass); }
使用 Expression tress(如选项 4 中所说),并没有给出像直接调用 IL 代码那么多的灵活性,尽管它确实能防止你调用无效代码! 考虑到这一点,如果你发现自己确实需要 emit IL,我强烈推荐你使用性能卓越的 Sigil 库 ,因为它能在出错时提供更好的错误提示消息!
Tocy
翻译于 6天前
0 人顶
翻译得不错哦!