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

反射是慢还是快?实际演示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (67投票s)

2010年11月5日

CPOL

10分钟阅读

viewsIcon

118482

downloadIcon

693

本文演示了反射API在实际开发中的行为,并提供了几种提高其性能的方法。

引言

在谈了一些关于Reflection.Emit的注意事项后,我开始思考反射在实际场景中是如何工作的。在谷歌搜索之后,我看到了一些很棒的文章,每一篇都提到了反射很慢。很多人可能和我一样都听说过,从代码中调用反射API总是比直接调用要慢。嗯,这是对的。一些主要的反射方法,如GetXXXMethodInfo、PropertyInfo、FieldInfo等),比直接调用方法、属性或字段要慢大约100倍。在这篇文章中,我将尝试涵盖一些比普通方法调用慢得多的主要反射部分,并尝试解决这些问题。

反射是用来做什么的?

正如我已经告诉过你的关于反射和代码发出以及如何构建你的Lambda表达式树,了解何时使用反射是很有益的。是的……反射确实是一种后期绑定的方法,用于遍历对象层次结构或调用对象内部的方法。因此,如果你对你的对象层次结构不太了解,或者你偶然发现了一个应用程序外部的程序集,并且你需要解析对象并调用其方法,那么反射将是你的正确选择。

所以基本上,反射允许你创建插件类型的应用程序,你可以定义一个接口并将其发布给你的应用程序,然后让其他人为你创建这些插件,以便无缝地添加到你的应用程序中。好的。那么,它不适用于什么呢?

是的,基本上有些人试图基于反射来完成所有工作。不必要地使用反射会使你的应用程序成本非常高。例如,

ConstructorInfo cinfo= typeof(MyType).GetConstructor(
                     new Type[] { typeof(int), typeof(string) });
object createdObject = cinfo.Invoke(new object[] { 10, "Abhishek" });
MyType xyz = createdObject as MyType;

或者说使用

MyType typ = Activator.CreateInstance<MyType>(); 

真傻……

我看到一些人这样做。他们试图使用反射来完成所有事情,也许他们太喜欢反射了,或者他们已经创建了一个反射引擎,不想将未知类型与已知类型混淆。也许他们会通过将已知类型调用到用于处理未知类型的同一个模块来使他们的代码更清晰。

当你循环执行这些反射调用时,情况会变得更糟。例如,你即将设置运行时对象的数百个成员,并使用PropertyInfo来处理这100个设置器。是的,代码看起来会非常简单,但会牺牲性能。

当一个类型中存在大量成员,或者一个程序集中存在大量类型时,Type.InvokeMember通常非常慢,Activator.CreateInstance也是如此。是的,使用这些方法进行真正的后期绑定会使你的应用程序变得非常慢。

你应该在哪里避免反射

这是每个人普遍关心的问题。当你了解对象时,反射工作得很好。你对对象了解得越多,就越能避免搜索对象层次结构。我发现最耗费性能的方法之一是GetCustomAttributes,你试图从一个类型中查找一个属性,无论它是否被添加以对类型进行某些操作。如果你不知道自定义属性应该设置在哪里,或者你的属性设置了AttributeTargets.All,并且你使用

myassembly.GetCustomAttributes(true) 

额外的参数true意味着它将遍历所有类型、其方法、其属性并尝试找到你的属性。因此,它是一个遍历整个对象层次结构的调用。是的,这非常耗费性能。你必须避免这些调用。让我列出一些耗费性能的反射方法:

  • GetCustomAttributes
  • GetXX(PropertyInfo、MethodInfo、EventInfo、FieldInfo等)
  • Type.InvokeMember(当类型非常大时)
  • Activator.CreateInstance

还有其他一些。但是你应该总是尽量避免在循环中调用这些方法。

ASP.NET 网站中的反射

谈到 ASP.NET 网站,我们总是会想到性能和吞吐量。对于 ASP.NET 网站,最好避免过度使用反射,也许当你每次调用一个插入到应用程序中的成员都使用反射时,当你的应用程序有成千上万个调用时,它会使你的应用程序运行得越来越慢。这就是 DasBlog 的问题,它实际上使用了反射来处理每个发布的博客。Hanselman 指出了他们的问题,因为它试图使用反射查找每个成员。通过改变这些东西,网站的性能提高了 100 倍。

如果你的 Web 应用程序需要在高压条件下表现良好,我建议你在使用反射之前三思。如果你不使用反射来处理每个步骤,那么它的速度会快如闪电。

反射有多慢?

是的,现在是时候用实时数据来处理反射了。由于反射是处理类型的一种真正后期绑定的方法,因此单个程序集中的类型越多,性能就越慢。让我通过一个大型DLL(包含数百种类型)的例子来向你演示性能有多慢,并尝试使用和不使用反射来调用其中的一个成员,并测量你获得的性能提升。

Eric Gunnerson 也亲自在测量一些方法上投入了大量精力。在他的文章中(抱歉,文章已不可用),他根据对 .NET Framework 1.1 中单个方法进行的 100,000 次调用,得出了如下结果:

reflection.JPG 所以根据他的说法,Type.InvokeMemberDynamicMethod.Invoke运行时间最长。MethodBase.Invoke(或MethodInfo.Invoke)的性能减半,DynamicMethod.Delegate耗时更短,其次是普通委托调用,最后是接口调用和基于IL的指令。相当聪明,是吧?可能吧,但让我们构建自己的应用程序来演示这些场景。

让我举一个简单类的例子(我称之为DummyClass),它在一个大型程序集(包含547个类型)中的DummyNamespace中只有一个名为CallMe的方法。所以这个类看起来像这样:

namespace DummyNamespace
{
    public class DummyClass
    {
        public void CallMe(int x)
        {
           x = x + 10;
           return;
        }
    }
}

看起来很简单,是吧……好吧,让我们创建一个consumer类来检查它在压力情况下的表现。我将循环创建DummyClass的对象,并分别使用反射API和正常方式以随机值调用CallMe。为此,我创建了一个包含几个方法的ReflectionCalculator类。

public class ReflectionCalculator
{
    private Assembly _currentassembly;
    public Assembly CurrentAssembly
    {
        get
        {
            if (this._currentassembly == null)
                this._currentassembly = Assembly.LoadFrom("PredictADry.BizLogic.dll");
            return this._currentassembly;
        }
    }
    public void ReflectionBasedCall(int value)
    {
        Type dummyclass = this.CurrentAssembly.GetType("DummyNamespace.DummyClass",
                                                        true, true);
        MethodInfo dummyMethod = dummyclass.GetMethod("CallMe", BindingFlags.Instance | 
                         BindingFlags.Public, null, new Type[] { typeof(int) }, null);
        object dummyObject = Activator.CreateInstance(dummyclass);
        dummyMethod.Invoke(dummyObject, new object[] { value });
    }
        
    public void NormalCall(int value)
    {
        DummyClass oclass = new DummyClass();
        oclass.CallMe(value);

    }
}

因此,如果你仔细观察这两个调用,即ReflectionBasedCallNormalCall,你会发现,它们确实在做同样的事情。我把Assembly对象放在外面,这样我就可以只检查调用方法的逻辑。反射方法首先需要使用Assembly.GetType获取一个类型,这最终会搜索整个对象层次结构,然后是GetMethod,它在类型上查找。因此,程序集的大小越大,它们工作得越慢。最后是Activator.CreateInstance,它实际上创建了对象的实例。而另一个则正常地做着同样的事情。现在,如果我说我用这段代码调用这两个方法100000次,并打印出毫秒级的时间,它会看起来像这样:

static void Main(string[] args)
{
    DateTime currentTime;
    ReflectionCalculator calculator = new ReflectionCalculator();
    Console.WriteLine("Time Now : {0}", DateTime.Now.ToLocalTime());
    currentTime = DateTime.Now;

    for (int i = 0; i < 100000; i++)
    {
        calculator.NormalCall(i);
    }

    TimeSpan spannedtime = DateTime.Now.Subtract(currentTime);
    Console.WriteLine("Time Elapsed for Normal call : {0}", 
                             spannedtime.TotalMilliseconds);

    currentTime = DateTime.Now;
    for (int i = 0; i < 100000; i++)
    {
        calculator.ReflectionBasedCall(i);
    }
    spannedtime = DateTime.Now.Subtract(currentTime);
    Console.WriteLine("Time Elapsed for Reflection call : {0}", 
                            spannedtime.TotalMilliseconds);

    Console.ReadLine();
}

因此,输出将是

reflectiondemo.JPG

因此,你绝对可以了解到反射与普通调用相比有多慢。反射方法几乎比普通调用慢30倍。

缓存对象以放松

好吧,让我们使代码更友好,允许为每次调用缓存对象。因此,让我们创建几个属性来缓存反射调用的对象,看看它是如何工作的:

private Type dummyType;
public Type DummyType
{
    get 
    { 
        this.dummyType = this.dummyType ?? this.CurrentAssembly.GetType(
                                  "DummyNamespace.DummyClass", true, true); 
        return this.dummyType; 
    }
}
private MethodInfo method;
public MethodInfo Method
{
    get
    {
        this.method = this.method ?? this.DummyType.GetMethod("CallMe", 
                        BindingFlags.Instance | 
                        BindingFlags.Public, null, new Type[] { typeof(int) }, null);
        return this.method;
    }
}
private object dummyObject;
public object DummyObject
{
    get
    {
        if (this.dummyObject == null)
        {
            dummyObject = Activator.CreateInstance(this.DummyType);
        }
        return dummyObject;
    }
}
//More relaxed with caching of objects
public void ReflectionBasedCall(int value)
{
    this.Method.Invoke(this.DummyObject, new object[] { value });
}
public DummyClass MyClass { get; set; }
public void NormalCall(int value)
{
    this.MyClass = this.MyClass ?? new DummyClass();
    this.MyClass.CallMe(20);

}

所以这里,我们将每个对象缓存到其各自的属性中。这可能会让我们稍微放松一下,但请注意,这可能不是每次都具有相同方法的情况。但是为了看看Invoke方法的运行速度,让我们运行代码。

reflectiondemo1.JPG

所以由于应用了缓存,消耗的时间骤然减少了。因此,大部分时间都花在了GetTypeGetMethod上,甚至还有Activator.CreateInstance。但如果你看结果,它仍然是正常调用的15倍。

使用接口

在这些情况下,接口效果很好。让我们举一个存在于两个DLL中的接口的例子,其中DummyClass实际上实现了接口IDummy。天哪,接口在这些情况下表现得如此出色。如果你将所有内容都缓存起来,它的工作方式与正常的调用指令相同。

private IDummy dummyObject;
public IDummy DummyObject
{
    get
    {
        if (this.dummyObject == null)
        {
            dummyObject = Activator.CreateInstance(this.DummyType) as IDummy;
        }
        return dummyObject;
    }
}
public void ReflectionBasedCall(int value)
{
    //this.Method.Invoke(this.DummyObject, new object[] { value });
    this.DummyObject.CallMe(value);
}

因此输出是

reflectiondemo21.JPG

你一定会对此感到惊讶。是的,在这种情况下,它实际上比普通的IL指令表现得更好。

不,其实不是。从架构角度来看,调用轻量级接口比调用具体对象更好。如果你暂时缓存IDummy并进行比较,你会发现差异。持有缓存对象并通过其接口调用在性能上优于普通调用。所以让我们稍微修改一下代码看看:

private IDummy dummyObject;
public IDummy DummyObject
{
    get
    {
        if (this.dummyObject == null)
        {
            dummyObject = Activator.CreateInstance(this.DummyType) as IDummy;
        }
        return dummyObject;
    }
}
public void ReflectionBasedCall(int value)
{
    //this.Method.Invoke(this.DummyObject, new object[] { value });
    this.DummyObject.CallMe(value);
}

因此,如果你每次都创建两个对象,性能将是

reflectiondemo31.JPG

是的,没错,接口不会比原始IL指令表现得更好。但我们是否可能为所有调用都使用接口呢?你说得对,如果每次反射调用都需要接口,那会付出很多努力。所以也许是时候考虑其他方法了。

为每个方法使用动态创建的委托

嗯,我发现的另一种或者可能是最好的方法是使用基于MethodInfoPropertyInfo对象签名动态创建的委托。利用 C# 语言的动态灵活性或在运行时创建动态委托将使反射代码变得非常快。

基本上,其思想是为每种需要调用的MethodInfo类型构建泛型委托,并最终将其存储到一些内存缓存中。这样,你只需要为你的调用(基于签名)构建一些Action<>Func<>委托,并且每次调用它们时,你都可以维护一个列表,将每个委托与MethodInfo一起存储。因此,下次你需要调用相同的方法时,可以直接使用该委托。

为了做到这一点,我使用了System.Linq.Expressions。如果你不熟悉使用表达式树的动态特性,你可以阅读我的帖子来理解这个概念。我使用表达式是因为对我来说它似乎更好,但你也可以使用System.Reflection.Emit来构建这些调用包装器。
为了使用这个,我为属性、方法等创建了一些扩展方法。让我们以我们案例中的MethodInfo为例。

public static Action<object, T> CallMethod<T>(this MethodInfo methodInfo)
{
    if (!methodInfo.IsPublic) return null;

    ParameterExpression returnParameter = Expression.Parameter(typeof(object), "method");
    ParameterExpression valueArgument = Expression.Parameter(typeof(T), "argument");

    MethodCallExpression methodcaller = Expression.Call(
                                    Expression.ConvertChecked(returnParameter, 
                                    methodInfo.DeclaringType),
                                    methodInfo,
                                    Expression.Convert(valueArgument, typeof(T)));
    return Expression.Lambda<Action<object, T>>(methodcaller, 
                                        returnParameter, valueArgument).Compile();
}

因此,最终,我使用这个Lambda表达式来构建一个Action<object,T>,其中object表示我们将要执行方法的类的实例,而T表示参数列表中的Type参数。

如果你仔细观察上面的代码,你会明白我正在构建一个像这样的 lambda 表达式

(x, y) => return x.[method](y)

所以在这里也使用缓存,让我们使用代码动态构建我们的委托对象

# region Cache
private MethodInfo methodInfo;
public MethodInfo CachedMethodInfo
{
    get
    {
        this.methodInfo = this.methodInfo ?? this.GetMethodInfo();
        return this.methodInfo;
    }
}

private MethodInfo GetMethodInfo()
{
    Type dummyclass = this.CurrentAssembly.GetType("DummyNamespace.DummyClass", 
                                                    true, true);
    MethodInfo dummyMethod = dummyclass.GetMethod("CallMe", BindingFlags.Instance | 
                    BindingFlags.Public, null, new Type[] { typeof(int) }, null);

    return dummyMethod;
}
private object instance;
public object Instance
{
    get
    {
        this.instance = this.instance ?? Activator.CreateInstance(typeof(DummyClass));
        return this.instance;
    }
}
private Action<object, int> _methodcallDelegate = null;
public Action<object, int> MethodCallDelegate
{
    get
    {
        this._methodcallDelegate = this._methodcallDelegate ?? 
              this.CachedMethodInfo.CallMethod<int>();

        return this._methodcallDelegate;
    }
}

# endregion

public void CacheBasedDelegateCall(int value)
{
    this.MethodCallDelegate(this.Instance, value);
}

因此,我基本上创建了一个带有MethodCallDelegate的委托对象,并为每次方法调用缓存了它。MethodCallDelegate实际上调用了CachedMethodInfo对象中的扩展方法CallMethod,并将其存储在一个变量中。

注意:在你的实时应用程序中,当需要进行大量反射调用时,请将MethodDelegateMethodInfo对象一起缓存到集合中,这样你就可以尝试从集合中查找相同的方法,而不是每次调用都创建动态方法。

现在为了演示这一点,让我们在程序中再添加一个部分来调用这个CacheBasedDelegateCall。因此输出将如下所示:

reflectiondemo41.JPG

天哪,这种方法效果很好。是的,如果你能充分利用动态编译应用程序所花费的时间,那么应用程序将像以前一样快速运行。

就是这样。

结论

因此,我想我已经把事情说清楚了一些,希望你喜欢阅读。谢谢你,请发表你对此的看法。

编程愉快!

参考文献

历史

  • 初稿 - 2010年11月6日
© . All rights reserved.