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

大家都知道 .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");

这需要一定的成本,因为需要提取相关的元数据,并对其进行解析。运行时会帮我们维持一个内部缓存,缓存着所有字段/属性/方法。这个缓存由 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 会在你第一次调用的时候创建并缓存起来。

参数校验和错误处理

得到 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 类型的参数。这些工作不好的地方是常常需要装箱,这会增加额外的成本。希望这在将来会降到最低

安全性检查

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

反射机制耗时多少?

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

以下是由 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; }
    }
}

以及下面的通用代码,这里包含了所有可用的选项:

// 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);
}

选择1 - 缓存 PropertyInfo

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

// Setup code, done only once
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(@classallowNonPublicAccessorstrue);

[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);
}

它的缺点是你必须知道编译时的具体类型,也就是上面的代码中的 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);
}

关于 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(nonPublictrue))
    .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 参考讨论这篇文章

相关阅读

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

  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



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



阅读更多
换一批

为什么我的.net执行这么慢?

09-08

页面http://www.qq2234.cn/blo/14_695_1.aspx 打开一个页面要一分钟,在程序日志里面报错,希望大大们指点一下rnrnEvent code: 3005 rnEvent message: An unhandled exception has occurred. rnEvent time: 2010-9-8 11:33:54 rnEvent time (UTC): 2010-9-8 3:33:54 rnEvent ID: f25b668d1fb8447db1796d929009f1a3 rnEvent sequence: 346 rnEvent occurrence: 11 rnEvent detail code: 0 rn rnApplication information: rn Application domain: /LM/W3SVC/1404639508/Root/blo-1-129283876494572461 rn Trust level: Full rn Application Virtual Path: /blo rn Application Path: D:\www.qq2234.cn\qq2234\blo\ rn Machine name: GOODVPS-ACFB048 rn rnProcess information: rn Process ID: 4072 rn Process name: w3wp.exe rn Account name: NT AUTHORITY\NETWORK SERVICE rn rnException information: rn Exception type: HttpUnhandledException rn Exception message: Exception of type 'System.Web.HttpUnhandledException' was thrown. rn rnRequest information: rn Request URL: http://www.qq2234.cn/blo/goods.aspx?id=3750 rn Request path: /blo/goods.aspx rn User host address: 125.71.56.152 rn User: rn Is authenticated: False rn Authentication Type: rn Thread account name: NT AUTHORITY\NETWORK SERVICE rn rnThread information: rn Thread ID: 5 rn Thread account name: NT AUTHORITY\NETWORK SERVICE rn Is impersonating: False rn Stack trace: at System.Web.UI.Page.HandleError(Exception e)rn at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)rn at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)rn at System.Web.UI.Page.ProcessRequest()rn at System.Web.Util.AspCompatApplicationStep.System.Web.HttpApplication.IExecutionStep.Execute()rn at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)rn rn rnCustom event details: rnrn有关更多信息,请参阅在 http://go.microsoft.com/fwlink/events.asp 的帮助和支持中心。

没有更多推荐了,返回首页