65.9K
CodeProject 正在变化。 阅读更多。
Home

Fasterflect - 用于反射调用的快速简单 API

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (14投票s)

2009年8月9日

Apache

8分钟阅读

viewsIcon

72752

downloadIcon

971

了解 Fasterflect 的实现、API 和性能,它是 .NET 反射 API 的一个替代方案。

Fasterflect 1.0 (beta) 的二进制和源代码可以在上面的链接中找到。您还可以查看 Fasterflect CodePlex 页面 获取最新的开发代码。

背景

如果您认为 .NET 框架内置的反射 API 在许多情况下过于冗长且性能不佳,那么您并不孤单。我也这么认为。然而,在我的大多数应用程序开发中,能够编写反射代码是不可避免的(技术)要求。这就是我构建 Fasterflect(可以读作“Faster-flect”或“Fast-reflect”)作为 .NET 反射功能替代 API 的原因。

这个库的目标是使反射调用尽可能直接,同时提供比常规反射调用更好的性能。在本文中,我将介绍构建 Fasterflect 所采用的方法、其 API,并通过一些基准测试来衡量其性能。

实现

Fasterflect 基于 .NET 2.0 的动态方法 (Dynamic Methods) 构建。对于不熟悉动态方法的人来说,它基本上是一种允许在运行时使用通用中间语言 (CIL) 构建方法的功能。构建完成后,动态方法就可以强制转换为委托,并像普通的 CLR 委托一样调用。

让我们来讨论一下背景,看看这样的功能如何帮助 Fasterflect 的开发。假设我们想在一个对象的类型我们无法(或不希望)在编译时编译的类型上调用方法。我们可以使用内置的 .NET 反射 API 来实现这一点,如下所示:

string someTypeName = …; // load the type name from a config file
Type type = Assembly.GetExecutingAssembly().GetType(someTypeName); 
string methodToBeInvoked = …; // load method name from a config file
MethodInfo info = type.GetMethod(methodToBeInvoked, BindingFlags.Instance);
object result = info.Invoke(Activator.CreateInstance(type));

这可以工作。但是,这种反射调用的性能非常差,尤其是在我们多次执行方法时——这是大多数使用反射的应用程序中的常见场景。不仅如此,API 也不好用,尽管上面的代码可能是基于 .NET 反射的最不冗长的一段代码(例如,我们还没有将可见性、方法重载等因素考虑在内)。

后一个问题很容易解决,我们可以只提供一些包装类,提供更简单、更流畅的反射方法接口。Fasterflect API 通过抽象出大多数应用程序不需要处理的 `MethodInfo`、`PropertyInfo` 等概念来简化事情,并对最常用的场景做出假设(例如,当我们想通过反射设置字段的值时,我们通常不关心该字段是私有的还是公开的)。遵循这种方法将使 API 非常简单轻量。缺点是它不能在所有应用程序中作为 .NET 内置反射 API 的完整替代品。例如,如果您只需要为类型和方法查询元数据,而不需要用于调用,那么 Fasterflect 就帮不上忙。但根据我的经验,大多数应用程序只需要执行反射调用,因此我认为针对这些场景优化 API 是值得的。

前一个问题可以通过在运行时生成代码来解决。回到我们的示例,想法是,当 `someTypeName` 和 `methodToBeInvoked` 在运行时已经用配置文件中的值初始化(假设类型名称是 `Person`,方法名称是 `GetName`)时,我们已经有足够的信息来构造一个如下的非反射代码。 (假设 `GenericInvocator` 是一个只有一个方法的接口,object GenericInvoke(object),我们已经编写了该接口。)

public class ConcreteInvocator : GenericInvocator 
{
    public object Invoke(object target) 
    {
        var person = (Person)target;
        return target.GetName();
    }
}

在运行时生成代码后,我们可以使用 *csc.exe* 编译器或 CodeDomProvider 在线编译它。假设我们将代码编译成一个程序集,我们可以像下面这样加载并执行调用:

object obj = Assembly.LoadFrom(generatedDll).CreateInstance("ConcreteInvocator")
GenericInvocator personInvocator = obj as GenericInvocator;
string name = (string)personInvocator.Invoke(personObject);

请注意,我们只需要生成一次代码,然后就可以在所有后续调用中重复使用它,这将非常快,因为它只是直接调用 `person.GetName()`。

这就是想法。然而,生成和编译高级代码(或构建 CodeDOM 树)并不是唯一的选择。另一个选择是直接生成 CIL 并跳过所有编译开销。我们可以通过使用 Reflection Emit 命名空间中的类来在运行时构建 .NET 程序集、模块和类型,或者使用动态方法来实现这一点。在所有这些选项中,我认为动态方法是最合适的解决方案。我不会在这篇文章中提供这些选项的全面比较,而是重点介绍动态方法相对于其替代方案的三个关键优势:

  • 动态方法可以声明为跳过可见性。如果一个成员被声明为 private,动态方法的体仍然可以直接访问它,而不是通过反射。替代方案则不是这样。
  • 动态方法可以被垃圾回收,而使用替代方法构建的方法一旦加载到应用程序域就无法卸载。
  • 我们不需要编写代码来构建模块、类型等;相反,我们可以只编写动态方法的代码,.NET 会负责其余的。

生成一个动态方法来调用特定类型的一个无参数实例方法的代码如下所示。(假设委托 `MethodInvoker` 已声明为 public delegate object MethodInvoker(object target);。)

public MethodInvoker CreateDelegate(Type targetType, string methodName) 
{
    var method= new DynamicMethod("invoke", MethodAttributes.Static |   
                                  MethodAttributes.Public, CallingConventions.Standard, 
                                  typeof(object), new Type[0], targetType, true);
    ILGenerator generator = method.GetILGenerator();
    MethodInfo methodInfo =  targetType.GetMethod(methodName, BindingFlags.Instance);
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Castclass, targetType);
    generator.Emit(OpCodes.Callvirt, methodInfo);
    if (methodInfo.ReturnType == typeof(void))
    {
        generator.Emit(OpCodes.Ldnull);
    }
    else
    {
     if (methodInfo.ReturnType.IsValueType)
     {
         generator.Emit(OpCodes.Box, methodInfo.ReturnType);
     }
    }
    generator.Emit(OpCodes.Ret);
    return method.CreateDelegate(typeof(MethodInvoker));
}

现在,您可以通过返回的委托简单地调用动态方法:

MethodInvoker  invoker = CreateDelegate(personType, "GetName");
string name = (string)invoker.Invoke(personObject);

如果您已经熟悉 CIL,那么动态方法的代码应该会一目了然。如果不熟悉,您可以查阅互联网上大量的 CIL 文档。只有当您想了解 Fasterflect 的细粒度实现细节时,才需要这样做。如果您只需要有效地使用该库,或者只想获得足够的信息来决定是否使用它,那么前面关于实现细节的重点应该已经足够了。话虽如此,让我们探索 Fasterflect 提供的 API,并观察它带来的性能提升。

API

在探索 Fasterflect 提供的 API 之前,让我们先看看我们将用于在 `Person` 类上执行反射调用的类。这个类没有什么特别之处;它只是一个带有大量静态和/或实例构造函数、字段、属性、方法和索引器的 POCO。

class Person
{
    private int id;
    private int milesTraveled;
    public int Id
    {
        get { return id; }
        set { id = value; }
    }
    public string Name { get; private set; }
    private static int InstanceCount;

    public Person() : this(0) {}
    public Person(int id) : this(id, string.Empty) { }
    public Person(int id, string name)
    {
        Id = id;
        Name = name;
        InstanceCount++;
    }

    public char this[int index]
    {
        get { return Name[index]; }
    }

    private void Walk(int miles) 
    {
        milesTraveled += miles;
    }

    private static void IncreaseInstanceCount()
    {
        InstanceCount++;
    }

    private static int GetInstanceCount()
    {
        return InstanceCount;
    }
}

Fasterflect 提供两种 API,每种都有其优缺点。(实际上有三种 API,但第三种不是常用 API - 请查阅库代码文档了解更多细节。)

第一种 API 是默认 API。此 API 由一系列 `System.Type` 和 `System.Object` 的扩展方法组成。此 API 的优点是它非常易于使用。缺点是,虽然此 API 只是前面介绍的动态方法生成方法的包装器,但它比普通的 .NET 反射调用仅快 6-10 倍。原因是 API 和动态方法调用之间存在一些间接引用。这是拥有包装器 API 不可避免的成本,但这显然是 Fasterflect 未来版本中进一步优化的领域。

让我们通过一些代码演示来探索这个 API。我包含了很多注释来取代冗长的文字叙述。

// Load a type reflectively, just to look like real-life scenario
Type type = Assembly.GetExecutingAssembly().GetType("FasterflectSample.Person");

// Person.InstanceCount should be 0 since no instance is created yet
AssertTrue(type.GetField<int>("InstanceCount") == 0);

// Invokes the no-arg constructor
object obj = type.Construct();

// Double-check if the constructor is invoked successfully or not
AssertTrue(null != obj);

// Now, Person.InstanceCount should be 1
AssertTrue(1 == type.GetField<int>("InstanceCount"));

// What if we don't know the type of InstanceCount?  
// Just specify object as the type parameter
Console.WriteLine(type.GetField<object>("InstanceCount"));

// We can bypass the constructor to change the value of Person.InstanceCount
type.SetField("InstanceCount", 2);
AssertTrue(2 == type.GetField<int>("InstanceCount"));

// Let's invoke Person.IncreaseCounter() static method to increase the counter
// In fact, let's chain the calls to increase 2 times
type.Invoke("IncreaseInstanceCount")
    .Invoke("IncreaseInstanceCount");
AssertTrue(4 == type.GetField<int>("InstanceCount"));

// Now, let's retrieve Person.InstanceCount via the static method GetInstanceCount
AssertTrue(4 == type.Invoke<int>("GetInstanceCount"));

// If we're not interested in the return (e.g. only in the side effect), 
// we don't have to specify the type parameter (and can chain the result).
AssertTrue(4 == type.Invoke("GetInstanceCount")
                    .Invoke("GetInstanceCount")
                    .Invoke<int>("GetInstanceCount"));

// Now, invoke the 2-arg constructor
obj = type.Construct(new[] {typeof (int), typeof (string)}, 
                     new object[] {1, "Doe"});

// The id field should be 1, so is Id property
AssertTrue(1 == obj.GetField<int>("id"));
AssertTrue(1 == obj.GetProperty<int>("Id"));

// Now, modify the id
obj.SetField("id", 2);
AssertTrue(2 == obj.GetField<int>("id"));
AssertTrue(2 == obj.GetProperty<int>("Id"));

// Let's use the indexer to retrieve the character at index 1st
AssertTrue('o' == obj.GetIndexer<char>(new[] {typeof (int)}, new object[] {1}));

// We can chain calls
obj.SetField("id", 3).SetProperty("Name", "Buu");
AssertTrue(3 == obj.GetProperty<int>("Id"));
AssertTrue("Buu" == obj.GetProperty<string>("Name"));
 
// How about modifying both properties at the same time using an anonymous sample
obj.SetProperties(new {Id = 4, Name = "Nguyen"});
AssertTrue(4 == obj.GetProperty<int>("Id"));
AssertTrue("Nguyen" == obj.GetProperty<string>("Name"));

// Let's have the folk walk 6 miles (and try chaining again)
obj.Invoke("Walk", new[] { typeof(int) }, new object[] { 1 })
   .Invoke("Walk", new[] { typeof(int) }, new object[] { 2 })
   .Invoke("Walk", new[] { typeof(int) }, new object[] { 3 });

// Double-check the current value of the milesTravelled field
AssertTrue(6 == obj.GetField<int>("milesTraveled"));

第二种 API 称为缓存 API,使用起来稍微有些冗长,但能带来巨大的性能提升(通常比普通反射调用快几百倍)。其思想是,您可以获得对动态方法返回的委托的直接句柄,并重复使用它,而不是通过许多间接层(如默认 API)。以下是如何使用缓存 API:

// Load a type reflectively, just to look like real-life scenario
Type type = Assembly.GetExecutingAssembly().GetType("FasterflectSample.Person");

var range = Enumerable.Range(0, 10).ToList();

// Let's cache the getter for InstanceCount
StaticAttributeGetter count = type.DelegateForGetStaticField("InstanceCount");

// Now cache the 2-arg constructor of Person and playaround with the delegate returned
int currentInstanceCount = (int)count();
ConstructorInvoker ctor = type.DelegateForConstruct(new[] { typeof(int), typeof(string) });
range.ForEach(i =>
{
    var obj = ctor(i, "_" + i);
    AssertTrue(++currentInstanceCount == (int)count());
    AssertTrue(i == obj.GetField<int>("id"));
    AssertTrue("_" + i == obj.GetProperty<string>("Name"));
});

// Whatever thing we can do with the normal API, we can do with the cache API.
// For example:
AttributeSetter nameSetter = type.DelegateForSetProperty("Name");
AttributeGetter nameGetter = type.DelegateForGetProperty("Name");
object person = ctor(1, "Buu");
AssertTrue("Buu" == nameGetter(person));
nameSetter(person, "Doe");
AssertTrue("Doe" == nameGetter(person));

// Another example
person = type.Construct();
MethodInvoker walk = type.DelegateForInvoke("Walk", new[] { typeof(int) });
range.ForEach(i => walk(person, i));
AssertTrue(range.Sum() == person.GetField<int>("milesTraveled"));

性能

我构建了一个应用程序来展示每种调用类型的调用基准(包括构造函数、字段、属性、方法和索引器):直接调用、内置 .NET 反射、Fasterflect 默认 API 和 Fasterflect 缓存 API。您可以在源代码下载中查看基准测试应用程序的完整代码。在本文中,我仅列出了构造函数调用基准测试和方法调用基准测试的代码。

private static readonly int[] Iterations = new[] { 20000, 2000000 };
private static readonly object[] NoArgArray = new object[0];
private static readonly object[] ArgArray = new object[]{10};
private static readonly Type TargetType = typeof (Person);
private static readonly Person TargetPerson = new Person();
private static readonly Stopwatch Watch = new Stopwatch();

private static void RunConstructorBenchmark()
{
    ConstructorInfo ctorInfo = null;
    ConstructorInvoker invoker = null;;
    var initMap = new Dictionary<string, Action>
      {
            {"Init info", () => 
              { ctorInfo = typeof (Person).GetConstructor(BindingFlags.Instance | 
                           BindingFlags.NonPublic, null, new Type[0], null); }},
            {"Init ctorInvoker", () => 
              {invoker = typeof(Person).DelegateForConstruct();}}
      };
    var actionMap = new Dictionary<string, Action>
      {
            {"Direct ctor", () => new Person() },
            {"Reflection ctor", () => ctorInfo.Invoke(NoArgArray)},
            {"Fasterflect ctor", () => typeof(Person).Construct() },
            {"Fasterflect cached ctor", () => invoker(NoArgArray) },
      };
    Execute("Construction Benchmark", initMap, actionMap);
}

private static void RunMethodInvocationBenchmark()
{
    MethodInfo noArgMethodInfo = null;
    MethodInfo argMethodInfo = null;

    MethodInvoker noArgInvoker = null;
    MethodInvoker argInvoker = null;

    var initMap = new Dictionary<string, Action>
      {
            {"Init no-arg info", () => 
               { noArgMethodInfo = TargetType.GetMethod("Walk", 
                 BindingFlags.NonPublic | BindingFlags.Instance, 
                 null, new Type[0], null); }},
            {"Init arg info", () => 
               { argMethodInfo = TargetType.GetMethod("Walk", 
                 BindingFlags.NonPublic | BindingFlags.Instance, 
                 null, new Type[]{typeof(int)}, null); }},
            {"Init no-arg invoker", () => 
               { noArgInvoker = TargetType.DelegateForInvoke("Walk"); }},
            {"Init arg invoker", () => 
               { argInvoker = TargetType.DelegateForInvoke("Walk", 
                              new[] { typeof(int) }); }}
      };

    var actionMap = new Dictionary<string, Action>
      {
            {"Direct invoke", () => TargetPerson.Walk()},
            {"Direct invoke (arg)", () => TargetPerson.Walk(10)},
            {"Reflection invoke", () => 
                        noArgMethodInfo.Invoke(TargetPerson, NoArgArray)},
            {"Reflection invoke (arg)", () => 
                        argMethodInfo.Invoke(TargetPerson, ArgArray)},
            {"Fasterflect invoke", () => 
                        TargetPerson.Invoke("Walk")},
            {"Fasterflect invoke (arg)", () => 
                        TargetPerson.Invoke("Walk", 
                        new[]{typeof(int)}, ArgArray)},
            {"Fasterflect cached invoke", () => 
                        noArgInvoker(TargetPerson, NoArgArray)},
            {"Fasterflect cached invoke (arg)", () => 
                        argInvoker(TargetPerson, ArgArray)}
      };
    Execute("Method Benchmark", initMap, actionMap);
}

private static void Execute(string name, Dictionary<string, Action> initMap, 
                            Dictionary<string, Action> actionMap)
{
    Console.WriteLine("------------- {0} ------------- ", name);

    Console.WriteLine("*** Initialization");
    Measure(Watch, initMap, 1);
    Console.WriteLine();

    foreach (var iterationCount in Iterations)
    {
        Console.WriteLine("*** Executing for {0} iterations", iterationCount);
        Measure(Watch, actionMap, iterationCount);
        Console.WriteLine();
    }
    Console.WriteLine();
}

private static void Measure(Stopwatch watch, Dictionary<string, Action> actionMap, 
    int iterationCount)
{
    foreach (var entry in actionMap)
    {
        watch.Start();
        for (int i = 0; i < iterationCount; i++)
            entry.Value();
        watch.Stop();
        Console.WriteLine("{0,-35} {1,6} ms", entry.Key + ":", 
                          watch.ElapsedMilliseconds);
        watch.Reset();
    }
}

上述基准测试的结果如下。结果的关键亮点是:

  • Fasterflect 构建动态方法只需要几毫秒。而且,每个字段、方法、构造函数、属性或索引器只需要在每个应用程序中执行一次。动态方法由 Fasterflect 在后续调用中重复使用。
  • 当重复执行相同的调用相当多次(200 万次)时,默认 API 和缓存 API 的性能分别比内置 .NET 反射 API 的性能快约 6-10 倍和 200-400 倍。

结论

Fasterflect 允许您对构造函数、索引器、实例和静态字段、方法和属性执行反射调用。您应该根据自己的具体需求,在这两种 API 中选择一种,一种优化为简单性,一种优化为性能。通过本文,我希望为您提供了足够的信息来决定该库是否有用,以及足够多的示例来开始使用该库。

祝您编写反射代码愉快!

© . All rights reserved.