代码之家  ›  专栏  ›  技术社区  ›  Paolo

调用委托与方法的性能

  •  56
  • Paolo  · 技术社区  · 16 年前

    在这个问题之后- Pass Method as Parameter using C# 我个人的一些经验是,我想了解更多关于调用委托的性能,而不仅仅是用C语言调用方法的性能。

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

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


    编辑 -澄清我所说的委托和回调接口的含义。

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

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

    6 回复  |  直到 7 年前
        1
  •  70
  •   Jon Skeet    16 年前

    我还没有看到这种效果——我肯定从未遇到过瓶颈。

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

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

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

        2
  •  19
  •   Pete Montgomery    16 年前

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

    见 Joel Pobar 的博客。

        3
  •  18
  •   dsimcha    16 年前

    我发现完全不可信的是,委托比虚拟方法快得多或慢得多。如果有的话,学员应该以可以忽略的速度进行培训。在较低的层次上,委托通常实现如下(使用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中)紧密循环中的虚函数调用并不比非内联直接调用慢,前提是对于任何给定的循环运行,它们总是解析为相同的实际函数。

        4
  •  5
  •   nawfal Donny V.    13 年前

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

    考虑到变量已经是正确的类型(接口或委托),简单地调用它可以使委托获胜。

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

    而且,考虑到有些情况下,我们Simple不能预先存储委托(例如,在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

    在接口方法上检查操作(委托),获取 每次通话
    00:03:50.7326056

    在接口方法上检查操作(委托),获取 接口一次,每次调用的委托
    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();
            }
        }
    
    }
    
        5
  •  1
  •   Dorian Yeager    15 年前

    代理是容器这一事实呢?多播能力不会增加开销吗?当我们讨论这个问题时,如果我们把这个容器方面再推进一点呢?如果d是委托,执行d+=d,或者构建(上下文指针、方法指针)对的任意复杂有向图,没有什么能阻止我们。在哪里可以找到描述调用委托时如何遍历此图的文档?

        6
  •  -1
  •   Latency    7 年前

    可以找到台架试验 here .

    推荐文章