调用委托与方法的性能

根据这个问题—— 使用 C # 作为参数的传递方法和我的一些个人经验,我想知道更多关于调用委托与仅仅在 C # 中调用方法的性能的信息。

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

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


EDIT -阐明我所说的委托与回调接口的区别。

对于异步调用,我的类可以提供一个 OnComplete 事件和相关的委托,调用者可以订阅这些事件和委托。

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

64309 次浏览

我还没有看到这种效果——我肯定从来没有遇到过它成为瓶颈的情况。

下面是一个非常粗略的基准测试,它显示(至少在我的方框中)委托实际上是 再快点而不是接口:

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.0 b2大致相同) :

Interface: 5068
Delegate: 4404

现在我并不特别相信这意味着委托比接口更快... ... 但它让我相当确信它们并不比接口慢一个数量级。此外,这在委托/接口方法中几乎没有任何作用。显然,随着每次调用所做的工作越来越多,调用成本的影响将越来越小。

需要注意的一点是,在只使用单个接口实例的情况下,不会多次创建新的委托。这个 可以会导致一个问题,因为它会引起垃圾收集等。如果在循环中使用实例方法作为委托,您会发现在循环外声明委托变量、创建单个委托实例并重用它会更有效。例如:

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.
}

若要调用一个虚函数,需要执行以下操作:

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 # 中)紧密循环中的虚函数调用并不比非内联直接调用慢,前提是对于循环的任何给定运行,它们总是分解为相同的实际函数。

那么代表是容器这一事实又如何解释呢?多播能力不会增加开销吗?既然我们已经讨论了这个问题,那么如果我们进一步推进这个容器方面又会怎样呢?如果 d 是一个委托,没有什么禁止我们执行 d + = d; 或者构建一个任意复杂的有向图(上下文指针,方法指针)对。在哪里可以找到描述如何在调用委托时遍历此图的文档?

我做了一些测试(在.Net 3.5中... 稍后我将在家使用.Net 4进行检查)。 事实是: 获取对象作为接口,然后执行方法比从方法获取委托然后调用委托更快。

考虑到变量已经处于正确的类型(接口或委托) ,并且简单地调用它会使委托获胜。

由于某种原因,通过接口方法(可能是通过任何虚方法)获取委托的速度要慢得多。

而且,考虑到有些情况下我们无法预先存储委托(例如在调度中) ,这可能证明了为什么接口更快。

结果如下:

要获得真正的结果,请在发布模式下编译并在 VisualStudio 之外运行它。

查了两次直拨电话
00:00:005834988
00:00:00.5997071

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

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

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

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

检查接口方法上的 Action (委托) ,两者都处于 每通电话
00:03:50.7326056

在接口方法上检查 Action (委托) ,获取 接口一次,委托在每次调用
你好,你好,你好,你好

在接口方法上检查 Action (委托) ,两个都获取一次
00:00:040036530

如您所见,直接呼叫非常快。 之前存储接口或委托,然后只调用它真的很快。 但是,必须获得一个委托要比获得一个接口慢得多。 必须通过接口方法(或虚方法,不确定)获得委托非常缓慢(将获得一个对象作为接口的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();
}
}


}