调用代理与方法的性能

时间:2010-01-17 21:30:41

标签: c# .net performance delegates

关注这个问题 - Pass Method as Parameter using C#以及我的一些个人经历​​,我想更多地了解调用委托与在C#中调用方法的性能。

尽管代表非常方便,但我有一个应用程序通过委托做了很多回调,当我们重写这个以使用回调接口时,我们得到了一个数量级的速度改进。这是使用.NET 2.0所以我不确定3和4的情况如何变化。

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


编辑 - 澄清代表与回调接口的含义。

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

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

5 个答案:

答案 0 :(得分:73)

我没有看到这种效果 - 我当然从来没有遇到过它是瓶颈。

这是一个非常粗略和准备好的基准测试,显示(无论如何我的盒子上)代表实际上更快而不是接口:

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

现在我没有特别的信念,这意味着代表真正比接口更快......但它让我相信它们不会慢一个数量级。另外,这在委托/接口方法中几乎没有做任何事情。显然,调用成本会随着每次调用的工作量越来越少而产生越来越小的差异。

要注意的一件事是,您没有多次创建新的委托,而只使用单个接口实例。这个可能引起问题,因为它会引发垃圾收集等。如果你在循环中使用实例方法作为委托,你会发现在循环外声明委托变量更有效率,创建一个委托实例并重用它。例如:

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

这可能是您遇到的问题吗?

答案 1 :(得分:20)

从CLR v 2开始,委托调用的成本非常接近虚拟方法调用的成本,虚拟方法调用用于接口方法。

请参阅Joel Pobar的博客。

答案 2 :(得分:18)

我发现委托比虚拟方法更快或更慢是完全不可信的。如果有的话,代表应该可以忽略不计。在较低级别,代理通常实现类似(使用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#),紧密循环中的虚函数调用并不比非内联直接调用慢,前提是对于任何给定的循环运行,它们总是解析为相同的实函数

答案 3 :(得分:6)

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

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

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

并且,考虑到有些情况我们简单无法预先存储委托(例如,在Dispatches中),这可能证明接口更快的原因。

结果如下:

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

  

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

     

检查接口呼叫,每次呼叫时获取接口
  00:00:05.8998212

     

检查接口呼叫,获取界面一次   00:00:05.3163224

     

检查动作(委托)呼叫,在每次呼叫时获取动作
  00:00:17.1807980

     

检查动作(委托)呼叫,获取动作一次   00:00:05.3163224

     

通过接口方法检查Action(委托),同时获取两者   每次通话
  00:03:50.7326056

     

通过接口方法检查Action(委托),获取   接口一次,每次通话的代表
  00:03:48.9141438

     

通过接口方法检查操作(委托),同时获取两次   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();
        }
    }

}

答案 4 :(得分:1)

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