大家都知道 .NET 的反射很慢,但是为什么会出现这种情况呢?这篇文章会带你寻找这个问题的真正原因。

CLR 类型系统的设计目标

原因之一是,在设计的时候反射本身就不是以高性能为目标的,可以参考Type System Overview - ‘Design Goals and Non-goals’(类型系统概览 - ‘设计目标和非目标’):

目标

  • 运行时通过快速执行(非反射)代码访问需要的信息。

  • 编译时直接访问所需要的信息来生成代码。

  • 垃圾回收/遍历栈可以访问需要信息而不需要锁或分配内存。

  • 一次只加载最少量的类型。

  • 类型加载时只加载最少需要加载的类型。

  • 类型系统的数据结构必须在 NGEN 映像中保存。

非目标

  • 元数据的所有信息能直接反射 CLR 数据结构。

  • 快速使用反射。

参阅出处相同的 Type Loader Design - ‘Key Data Structures’(类型加载器设计 - ‘关键数据结构’):

EEClass

MethodTable(方法表)数据分为“热”和“冷”两种结构,以提高工作集和缓存的利用率。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(..) 的存根

  • System.RuntimeMethodHandle.InvokeMethod(..) 的存根 - 链接

即使不点击链接,想必你也能直观感受到改方法执行的大量代码。参考示例:System.RuntimeMethodHandle.InvokeMethodis 超过 400 行代码!

那么,它具体在做什么?

获取方法信息

要使用反射来调用字段/属性/方法,你必须获得 FieldInfo/PropertyInfo/MethodInfo,使用这样的代码:

Type t = typeof(Person);
FieldInfo m = t.GetField("Name");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 yetnull, 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, Static4 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; }
} Program
{private int TestField1;private string TestField2;private static string TestField3;private int TestProperty1 { get; set; }
}

看来运行时会筛选已经创建过的东西,这意味着调用 GetFeild 或 GetFields 不需要多大代价。对于 GetMethod 和 GetProperty 来说也是如此,MethodInfo 或 PropertyInfo 会在你第一次调用的时候创建并缓存起来。

参数校验和错误处理

得到 MethodInfo 之后,如果调用它的 Invoke 方法,会要处理很多事项。假设编写代码如下:

PropertyInfo stringLengthField = typeof(string).GetProperty("Length", BindingFlags.Instance | BindingFlags.Public);
var length = stringLengthField.GetGetMethod().Invoke(new Uri(), new object[0]);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(..)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 类型的参数。这些工作不好的地方是常常需要装箱,这会增加额外的成本。希望这在将来会降到最低。

安全性检查

另一个主要任务是多重安全性检查。例如,你不允许使用反射来任意调用你想调用的方法。这里存在一些限制的或 ‘危险方法’,只能由可信度高的 .NET 框架代码调用。除了黑名单外,还有动态安全检查,它由调用时必须检查的的当前代码访问安全权限决定。

反射机制耗时多少?

了解反射的实际操作后,我们来看看实际耗时。请注意,这些基准测试是通过反射直接比较读/写属性来完成的。在 .NET 中属性是一对 Get/Set 方法,这是由编译器生成的,但当属性只包含一个简单的内嵌字段时,.NET JIT 会使用内联 Get/Set 方法以提升性能。这意味着使用反射访问属性可能会遇到反射性能最差的情况,但它会被选择是因为这是最常见的用例,数据位于 ORMs, Json 序列化/反序列化库和对象映射工具中。

以下是由 BenchmarkDotNet 提供的原始结果,后面是在2个单独的表中显示的相同结果。 (全部Benchmark代码由此下载 )

读取属性值(‘Get’)

写属性值(‘Set’)

我们可以清楚地看到,正常的反射代码(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; }}
}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;
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);
}class.GetProperty("Data", bindingFlags);return (string)property.GetValue(testClass, null);
}

选择1 - 缓存 PropertyInfo

接下来,我们通过保存引用至 PropertyInfo 以获得速度上的少量提升,而不是每次都去获取。但即使这样,与直接访问属性相比,也仍然慢得多,这就表明在反射的“调用”部分成本很高。

// Setup code, done only once
PropertyInfo cachedPropertyInfo = @class.GetProperty("Data", bindingFlags);[Benchmark]
public string GetViaReflection()
{    return (string)cachedPropertyInfo.GetValue(testClass, null);
}
PropertyInfo cachedPropertyInfo = @class.GetProperty("Data", bindingFlags);[Benchmark]
public string GetViaReflection()
{    return (string)cachedPropertyInfo.GetValue(testClass, null);
}

选择2 - 使用 FastMember

这里使用了 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 accessor = TypeAccessor.Create(@class, allowNonPublicAccessors: true);[Benchmark]
public string GetViaFastMember()
{return (string)accessor[testClass, "Data"];
}

注意,与其他选择稍有不同,它创建了一个 TypeAccessor 来访问类型中的所有属性,而不仅是某一个。这带来的负面影响是会导致运行时间变长,因为它在内部首先要为你请求的属性(这个例子中是‘Data’)创建委托,然后再获取其值。不过这种开销是很小的,FastMember 仍然比其它反射方法更快,也更易用。所以我建议你先去看看。

这个选择及随后的选择将反射代码转换委托,这样就可以直接调用而不再需要每次都进行反射,速度因此得到提升!

必须指出创建一个委托需要一定的成本(可以从 ‘相关阅读’ 了解更多)。总之,速度提升是因为我们在其中进行过一次大投入(安全检查等)并保存了一个强类型的委托,之后我们只要稍微付出一点就可以一次次调用。如果反射只进行一次,那你大可不必使用这些技术。但是如果你只进行一次反射操作,它也不会出现性能瓶颈,你就完全不用在乎它会变慢!

通过委托读某个属性仍然不如直接访问来得快,因为 .NET JIT 不会将对委托方法的调用进行内联优化,而直接访问属性则会。因此即使使用委托,我们也需要为调用方法付出成本,而直接访问属性就不会。

选项3——创建代理(Delegate)

在这个选项中,我们使用 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);
}
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。

选项4——编译表达式树(Compiled Expression Trees)

这里我们生成一个 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);
}
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(使用表达式树的快速反射机制)“博客下载。

选项 5——IL Emit 动态代码生成

最后,虽然“权力越大,责任越大”,但这里我们还是使用最底层的方法调用原始 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);
}
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 库,因为它能在出错时提供更好的错误提示消息!

小结

如果(也只是如果)你发现自己在使用反射的时候有性能问题,有一些办法可以让它变得更快。获得这些速度提升是因为委托带来的对属性/字段/方法进行直接访问,这避免了每次进行反射的开销。

请在 /r/programming 和 /r/csharp 参考讨论这篇文章

相关阅读

  • FastExpressionKit - 一个让反射更快的小库

  • 反射正真的慢吗?

  • 反射为什么这么慢?

  • 是否值得使用.NET的反射?

  • 反射为何慢

  • 反射:使用反射仍然“不好”或“慢”?自2002年以来反射有怎样的改变?

  • 使用委托改善反射的性能

  • C#.Net 调用父辈虚方法(C# 中就是 base.base) - 第I部分、第II部分、第III部分

  • ‘探索委托,让反射飞’

  • Fasterflect vs HyperDescriptor vs FastMember vs Reflection

下面是创建委托时的调用栈或代码流,作为参考

  1. Delegate CreateDelegate(Type type, MethodInfo method)

  2. Delegate CreateDelegate(Type type, MethodInfo method, bool throwOnBindFailure)

  3. Delegate CreateDelegateInternal(RuntimeType rtType, RuntimeMethodInfo rtMethod, Object firstArgument, DelegateBindingFlags flags, ref StackCrawlMark stackMark)

  4. Delegate UnsafeCreateDelegate(RuntimeType rtType, RuntimeMethodInfo rtMethod, Object firstArgument, DelegateBindingFlags flags)

  5. bool BindToMethodInfo(Object target, IRuntimeMethodInfo method, RuntimeType methodType, DelegateBindingFlags flags);

  6. FCIMPL5(FC_BOOL_RET, COMDelegate::BindToMethodInfo, Object* refThisUNSAFE, Object* targetUNSAFE, ReflectMethodObject *pMethodUNSAFE, ReflectClassBaseObject *pMethodTypeUNSAFE, int flags)

  7. COMDelegate::BindToMethod(DELEGATEREF *pRefThis, OBJECTREF *pRefFirstArg, MethodDesc *pTargetMethod, MethodTable *pExactMethodType, BOOL fIsOpenDelegate, BOOL fCheckSecurity

原文地址:https://www.oschina.net/translate/why-is-reflection-slow

作者:jiankunking 出处:http://blog.csdn.net/jiankunking

为什么 .NET 的反射这么慢?相关推荐

  1. Java 反射 (快速了解反射)

    反射的概念 JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意方法和属性:这种动态获取信息以及动态调用对象方法的功能称为java ...

  2. Python Day26:多态、封装、内置函数:__str__、__del__、反射(反省)、动态导入模块...

    ## 多态 ```python OOP中标准解释:多个不同类型对象,可以响应同一个方法,并产生不同结果,即为多态 多态好处:只要知道基类使用方法即可,不需要关心具体哪一个类的对象实现的,以不变应万变, ...

  3. [AutoMapper]反射自动注册AutoMapper Profile

    AutoMapper 帮我我们方便管理物件跟物件之间属性值格式转换 模型转换 这里有两个类别 UserInfoModel 当作我们从DB捞取出来模型资料 public class UserInfoMo ...

  4. 2022-2028年中国反射偏光膜行业市场研究及前瞻分析报告

    [报告类型]产业研究 [出版时间]即时更新(交付时间约3个工作日) [发布机构]智研瞻产业研究院 [报告格式]PDF版 本报告介绍了反射偏光膜行业相关概述.中国反射偏光膜行业运行环境.分析了中国反射偏 ...

  5. Go 学习笔记(39)— Go 反射

    本文参考 http://c.biancheng.net/golang/reflect/ 反射是把双刃剑,功能强大但代码可读性并不理想,若非必要并不推荐使用反射. 1. 反射概念 反射是指在程序运行期对 ...

  6. PHP的Reflection反射机制

    原文地址: http://www.nowamagic.net/php/php_Reflection.php PHP5添加了一项新的功能:Reflection.这个功能使得程序员可以 reverse-e ...

  7. 需要反射时使用dynamic

    //使用dynamic的写法 dynamic fileExplorerData = _currentFolder.FileExplorerData; var data = fileExplorerDa ...

  8. c 与java 反射性能_谈谈Java 反射的快慢

    [相关学习推荐:java基础教程] 反射到底是好是坏 说到Java 中的反射,初学者在刚刚接触到反射的各种高级特性时,往往表示十分兴奋,甚至会在一些不需要使用反射的场景中强行使用反射来「炫技」.而经验 ...

  9. Java的反射作用_浅析Java 反射机制的用途和缺点

    反射的用途 Uses of Reflection Reflection is commonly used by programs which require the ability to examin ...

  10. java反射最佳实践,java反射性能测试分析

    java反射性能测试分析 java有别于其他编程语言而让我着迷的特性有很多,其中最喜欢的是接口设计,他让我们设计的东西具有美感.同样反射也是我比较喜欢的一个特性,他让程序自动运行,动态加载成为了可能, ...

最新文章

  1. 卸载破解的Navicat!操作所有的数据库靠它就够了!
  2. 人工智能中图神经网络GNN是什么?
  3. 吸墨网iPhone手机客户端界面设计
  4. php操作mysql数据库的扩展有哪些_8.PHP操作MySQL数据库(Mysqli扩展)
  5. QT自定义饼图的外观
  6. IBM Bluemix体验:Containers持久存储
  7. MySQL 高级repeat循环
  8. 【Xamarin挖墙脚系列:IOS-关于手机支持的屏幕方向】
  9. Android 图片相关整理
  10. SpringCloud工作笔记076--- CheckStyle插件提高java代码质量
  11. 【白皮书分享】中国新能源汽车供应链白皮书2020.pdf(附下载链接)
  12. Spring 自带的一些工具类
  13. 如何使用kafka增加topic的备份数量,让业务更上一层楼
  14. 这样选择报表系统,才能更好的进行企业管理
  15. 零基础搭建微信小程序商城系统
  16. 线性调频信号的时频域分析
  17. 武田2020财年第三季度业绩彰显增长加速和持续的韧性;确认了2020财年全年管理层指引,并上调了自由现金流以及列报每股盈利的预测
  18. IBM 2005-B16 SAN光纤交换机学习笔记
  19. C语言是否能用memcmp函数比较结构体
  20. 华科计算机学院专业课,华中科技大学计算机专业课程表.xls

热门文章

  1. van访谈_ElectricSheepCompany访谈
  2. 西安邮电大学计算机专业怎样,计算机怎么样?西安理工大学和西安邮电大学哪个比较好?...
  3. ios中录音功能的实现AudioSession的使用
  4. 散列查找(重点讲解查找失败的ASL) 习题集
  5. 工业相机——黑白相机像素格式排列解析
  6. LED点阵屏“鬼影”现象的分析和解决
  7. 对于ANDROID 5.0及其以上版本WIFI图标上显示感叹号的原因分析及解决方法
  8. 如何在64位Win7下使用震动手柄
  9. I2C学习笔记——I2C协议学习
  10. 弟中弟的Leetcode总结——数组类(四)