调用委托与方法的性能

在这个问题之后 – 传递方法作为参数使用C#和我的一些个人经验,我想了解更多关于调用委托的performance,而不是只调用C#中的方法。

虽然代表非常方便,但我有一个应用程序,通过委托做了很多的callback,当我们重写这个使用callback接口时,我们得到了一个数量级的速度提升。 这是与.NET 2.0,所以我不知道3和4如何变化。

在编译器/ CLR中如何调用内部处理的委托,以及这如何影响方法调用的性能?


编辑 – 澄清我的意思是代表与callback接口。

对于asynchronous调用,我的类可以提供调用者可以订阅的OnComplete事件和关联的委托。

或者,我可以使用调用者实现的OnComplete方法创build一个ICallback接口,然后将自己注册到将在完成时调用该方法的类(即Java处理这些事情的方式)。

我没有看到这种效果 – 我从来没有遇到过这个瓶颈。

这里有一个非常粗糙和准备好的基准,它显示(在我的盒子上)代表实际上比接口更快

using System; using System.Diagnostics; interface IFoo { int Foo(int x); } class Program : IFoo { const int Iterations = 1000000000; public int Foo(int x) { return x * 3; } static void Main(string[] args) { int x = 3; IFoo ifoo = new Program(); Func<int, int> del = ifoo.Foo; // Make sure everything's JITted: ifoo.Foo(3); del(3); Stopwatch sw = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { x = ifoo.Foo(x); } sw.Stop(); Console.WriteLine("Interface: {0}", sw.ElapsedMilliseconds); x = 3; sw = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { x = del(x); } sw.Stop(); Console.WriteLine("Delegate: {0}", sw.ElapsedMilliseconds); } } 

结果(.NET 3.5; .NET 4.0b2大致相同):

 Interface: 5068 Delegate: 4404 

现在我没有特别的信念,这意味着代表真的比接口更快……但是这让我相当确信它们不是一个数量级的慢。 另外,在代理/接口方法中几乎没有做任何事情。 显然,每次调用的工作量越来越多,调用成本越来越差。

有一点需要注意的是,如果只使用一个接口实例,则不会多次创build新的委托。 这可能会导致一个问题,因为它会引发垃圾收集等。如果您将一个实例方法用作循环中的委托,您会发现在循环外声明委托variables,创build单个委托实例并重用它。 例如:

 Func<int, int> del = myInstance.MyMethod; for (int i = 0; i < 100000; i++) { MethodTakingFunc(del); } 

比以下更有效率:

 for (int i = 0; i < 100000; i++) { MethodTakingFunc(myInstance.MyMethod); } 

这可能是你所看到的问题吗?

由于CLR v 2,委托调用的代价非常接近虚拟方法调用的成本,用于接口方法。

见Joel Pobar的博客。

我觉得委托人比虚拟方法快得多或慢得多,这是完全不可信的。 如果有什么代表应该是快速可以忽略不计。 在较低层次上,代表通常实施类似(使用C风格的符号,但请原谅任何小的语法错误,因为这只是一个例证):

 struct Delegate { void* contextPointer; // What class instance does this reference? void* functionPointer; // What method does this reference? } 

调用委托的工作原理如下所示:

 struct Delegate myDelegate = somethingThatReturnsDelegate(); // Call the delegate in de-sugared C-style notation. ReturnType returnValue = (*((FunctionType) *myDelegate.functionPointer))(myDelegate.contextPointer); 

一个翻译成C的类就是这样的:

 struct SomeClass { void** vtable; // Array of pointers to functions. SomeType someMember; // Member variables. } 

要调用一个vritual函数,你可以执行以下操作:

 struct SomeClass *myClass = someFunctionThatReturnsMyClassPointer(); // Call the virtual function residing in the second slot of the vtable. void* funcPtr = (myClass -> vtbl)[1]; ReturnType returnValue = (*((FunctionType) funcPtr))(myClass); 

它们基本上是相同的,除了使用虚拟函数时,您需要通过额外的间接层来获取函数指针。 然而,这个额外的间接寻址层通常是免费的,因为现代的CPU分支预测器会猜测函数指针的地址,并且在查找函数的地址的同时推测性地执行它的目标。 我发现(虽然在D中,而不是在C#中)在紧密循环中的虚函数调用没有比非内联的直接调用慢,假定对于任何给定的循环运行,它们总是parsing为相同的实函数。

我做了一些testing(在.Net 3.5 …以后我会在家里使用.Net 4)。 事实是:获取一个对象作为一个接口,然后执行该方法比从一个方法获得一个委托,然后调用委托更快。

考虑到variables已经在正确的types(接口或委托)和简单的调用它使委托胜利。

出于某种原因,通过接口方法获取委托(可能通过任何虚拟方法)慢得多。

而且,考虑到有些情况我们简单的不能预先存储委托(比如在Dispatches中),这可能certificate为什么接口更快。

结果如下:

要获得真实的结果,请在Release模式下编译并在Visual Studio外运行。

直接检查两次
00:00:00.5834988
00:00:00.5997071

检查接口调用,在每次调用时获取接口
00:00:05.8998212

检查接口调用,获取一次接口
00:00:05.3163224

检查操作(委托)调用,在每次调用时获取操作
00:00:17.1807980

检查Action(委托)调用,获取Action一次
00:00:05.3163224

通过接口方法检查Action(委托),在每次调用时获取
00:03:50.7326056

通过接口方法检查Action(委托),获取接口一次,委托在每个调用
00:03:48.9141438

通过接口方法检查Action(委托),获取一次
00:00:04.0036530

正如你所看到的那样,直接的电话真的很快。 之前存储接口或委托,然后只调用它真的很快。 但是必须得到一个委托比获取一个接口要慢。 不得不通过一个接口方法(或虚拟方法,不确定)获得一个委托是非常慢的(比较5秒获得一个对象作为一个接口,差不多4分钟做同样的行动)。

生成这些结果的代码在这里:

 using System; namespace ActionVersusInterface { public interface IRunnable { void Run(); } public sealed class Runnable: IRunnable { public void Run() { } } class Program { private const int COUNT = 1700000000; static void Main(string[] args) { var r = new Runnable(); Console.WriteLine("To get real results, compile this in Release mode and"); Console.WriteLine("run it outside Visual Studio."); Console.WriteLine(); Console.WriteLine("Checking direct calls twice"); { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { r.Run(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { r.Run(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking interface calls, getting the interface at every call"); { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { IRunnable interf = r; interf.Run(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking interface calls, getting the interface once"); { DateTime begin = DateTime.Now; IRunnable interf = r; for (int i = 0; i < COUNT; i++) { interf.Run(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) calls, getting the action at every call"); { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { Action a = r.Run; a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) calls, getting the Action once"); { DateTime begin = DateTime.Now; Action a = r.Run; for (int i = 0; i < COUNT; i++) { a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) over an interface method, getting both at every call"); { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { IRunnable interf = r; Action a = interf.Run; a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) over an interface method, getting the interface once, the delegate at every call"); { DateTime begin = DateTime.Now; IRunnable interf = r; for (int i = 0; i < COUNT; i++) { Action a = interf.Run; a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) over an interface method, getting both once"); { DateTime begin = DateTime.Now; IRunnable interf = r; Action a = interf.Run; for (int i = 0; i < COUNT; i++) { a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.ReadLine(); } } } 

代表是容器的事实呢? 多播能力不会增加开销吗? 当我们谈到这个问题的时候,如果我们把这个容器方面推进一点呢? 如果d是委托人,则不执行d + = d; 或者构build任意复杂的(上下文指针,方法指针)对的有向图。 我在哪里可以find描述代理被调用时如何遍历这个graphics的文档?