文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

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

CLR 类型系统的设计目标

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

目标

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

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

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

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

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

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

第 1 段(可获 1.81 积分)

非目标

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

  • 快速使用反射。

相关内容,请参考: Type Loader Design - ‘Key Data Structures’(类型加载器设计 - ‘关键数据结构’):

EEClass

MethodTable (方法表)数据分为“热”和“冷”两种结构,以提高工作集和缓存的利用率。MethodTable 本身只存储程序稳定状态的“热”数据。EEClass 存储“冷”数据,它们通常是类型加载、JITing 或反射所需要的。每个 MethodTable 指向一个 EEClass。

反射运行方式

第 2 段(可获 1.14 积分)

所以我们了解到,保证反射的速度很快并不是它的设计目标,那么额外花销的时间都在做什么呢?

此时有几件事情正在发生,为了阐明这些点,让我们来看一下托管代码和非托管代码在反射时所经历的调用堆栈。

  • System.Reflection.RuntimeMethodInfo.Invoke(..) - 源码传送门
    • 调用 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..)
  • System.RuntimeMethodHandle.PerformSecurityCheck(..) - 源码传送门
    • 调用 System.GC.KeepAlive(..)
  • System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..) - 源码传送门
    • 调用桩函数 System.RuntimeMethodHandle.InvokeMethod(..)
  • 桩函数 System.RuntimeMethodHandle.InvokeMethod(..) - 源码传送门
第 3 段(可获 1.11 积分)

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

那么,它具体在做什么?

获取方法信息

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

Type t = typeof(Person);      
FieldInfo m = t.GetField("Name");
第 4 段(可获 1.13 积分)

这需要一定的成本,因为需要提取相关的元数据,并对其进行解析。运行时会帮我们维持一个内部缓存,缓存着所有字段/属性/方法。这个缓存由 RuntimeTypeCache 类实现,用法示例在 RuntimeMethodInfo 类中.。

运行 gist 中的代码你可以看到缓存的何运作方式,它恰如其分地使用反射检查运行时内部!

gist 上的代码会在你使用反射获得 FieldInfo 之前输出下列内容:

第 5 段(可获 1.21 积分)
  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
第 6 段(可获 0.19 积分)

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 方法,会要处理很多事项。假设编写代码如下:

第 7 段(可获 0.98 积分)
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(..)
第 8 段(可获 0.13 积分)

这是因为我们获得了 String 类 Length 属性的 PropertyInfo,但是却在 Uri 对象上调用它,显然,这是个错误的类型!

此外,你还必须在调用方法时对传递给方法的参数进行校验。为了能传递参数,反射 API 使用了一个 object 的数组作为参数,其中每一个元素表示一个参数。所以,如果你使用反射来调用 Add(int x, int y) 方法,你得调用 methodInfo.Invoke(.., new [] { 5, 6 })。运行时会对传入参数的数量和类型进行检查,在这个示例中你要确保是 2 个 int 类型的参数。这些工作不好的地方是常常需要装箱,这会增加额外的成本。希望这在将来会降到最低

第 9 段(可获 1.75 积分)

安全性检查

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

反射机制耗时多少?

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

第 10 段(可获 2.46 积分)

下图是一些由 BenchmarkDotNet 展示的原测试结果,随后是分两张表格来展示的测试结果。(完整版 Benchmark 源码可在此处查看

Reflection Benchmark Results

属性的读(‘Get’)操作

MethodMeanStdErrScaledBytes Allocated/Op
GetViaProperty0.2159 ns0.0047 ns1.000.00
GetViaDelegate1.8903 ns0.0082 ns8.820.00
GetViaILEmit2.9236 ns0.0067 ns13.640.00
GetViaCompiledExpressionTrees12.3623 ns0.0200 ns57.650.00
GetViaFastMember35.9199 ns0.0528 ns167.520.00
GetViaReflectionWithCaching125.3878 ns0.2017 ns584.780.00
GetViaReflection197.9258 ns0.2704 ns923.080.01
GetViaDelegateDynamicInvoke842.9131 ns1.2649 ns3,931.17419.04
第 11 段(可获 1.58 积分)

属性的写(‘Set’)操作

MethodMeanStdErrScaledBytes Allocated/Op
SetViaProperty1.4043 ns0.0200 ns6.550.00
SetViaDelegate2.8215 ns0.0078 ns13.160.00
SetViaILEmit2.8226 ns0.0061 ns13.160.00
SetViaCompiledExpressionTrees10.7329 ns0.0221 ns50.060.00
SetViaFastMember36.6210 ns0.0393 ns170.790.00
SetViaReflectionWithCaching214.4321 ns0.3122 ns1,000.0798.49
SetViaReflection287.1039 ns0.3288 ns1,338.99115.63
SetViaDelegateDynamicInvoke922.4618 ns2.9192 ns4,302.17390.99

所以我们可以清楚的看到常见的反射代码(GetViaReflection和 SetViaReflection)与直接访问属性 (GetViaProperty和 SetViaProperty)相比,那是相当的慢啊。那么其他的结果会怎样呢,让我们来进一步的探索一下。

第 12 段(可获 1.66 积分)

设置

首先我们从 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;
第 13 段(可获 0.29 积分)

正常的反射

首先我们使用常规基准代码来表示我们的起始情况和“最坏情况”:

[Benchmark]
public string GetViaReflection()
{
    PropertyInfo property = @class.GetProperty("Data", bindingFlags);
    return (string)property.GetValue(testClass, null);
}

选择1 - 缓存 PropertyInfo

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

第 14 段(可获 0.86 积分)
// 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(@class, allowNonPublicAccessors: true);

[Benchmark]
public string GetViaFastMember()
{
    return (string)accessor[testClass, "Data"];
}
第 15 段(可获 0.34 积分)

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

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

第 16 段(可获 1.54 积分)

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

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

第 17 段(可获 1.78 积分)

选项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>,编译器会抛出一个异常!)。不过,在大多数情况下,使用反射不会遇到这么多麻烦。

第 18 段(可获 1.11 积分)

有效避免麻烦,请参阅 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);
}
第 19 段(可获 0.98 积分)

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

使用 Expression tress(如选项 4 中所说),并没有给出像直接调用 IL 代码那么多的灵活性,尽管它确实能防止你调用无效代码! 考虑到这一点,如果你发现自己确实需要 emit IL,我强烈推荐你使用性能卓越的 Sigil 库,因为它能在出错时提供更好的错误提示消息!

第 20 段(可获 1.26 积分)

小结

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

请在 /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)

 

第 21 段(可获 2.03 积分)

文章评论