【问题标题】:Performance of calling delegates vs methods调用委托与方法的性能
【发布时间】:2011-01-06 03:59:16
【问题描述】:

根据这个问题 - Pass Method as Parameter using C# 和我的一些个人经验,我想进一步了解调用委托与仅在 C# 中调用方法的性能。

虽然委托非常方便,但我有一个应用程序通过委托执行大量回调,当我们重写它以使用回调接口时,我们的速度提高了一个数量级。这是 .NET 2.0,所以我不确定 3 和 4 发生了什么变化。

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


编辑 - 澄清我所说的委托与回调接口的意思。

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

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

【问题讨论】:

标签: c# .net performance delegates


【解决方案1】:

我做了一些测试(在 .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

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

}

【讨论】:

  • 您可能不应该在运行委托的时间内包含获取委托。
  • 不错的基准测试,谢谢。我尝试了多种变体并确定:直接调用总是最快的;静态直接调用并不比实例成员直接调​​用快; .NET 4 对于直接调用较慢,但在某些其他情况下较快;用“/optimize+”编译会有帮助,但是“/debug-”和“/checked-”没有任何区别; "/platform:x64" 不会影响计时,但 "/platform:x86" 会(在某些情况下更快,在大多数情况下更慢);将测试分成自己的方法没有区别;将 Runnable 放在单独的程序集中没有任何区别。
  • Action 类不会增加一些开销吗?
【解决方案2】:

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

【讨论】:

  • 从概念上讲,没有理由支持多播委托必须在单目标情况下减慢调用速度。如果具有多个目标的委托将其内部方法指针设置为特殊的 ExecuteMultiDelegate 方法,并将其内部目标引用设置为包含 (Object,Method) 对的结构数组,则委托可以无条件地分派到其方法,而无需检查是否有多个目标。 ExecuteMultiDelegate 方法必须禁用一些正常的类型检查行为,但这应该是可行的。
  • 请注意,我刚刚描述的方法并不是 AFAIK MulticastDelegates 的实际实现方式,而是一种优化最常见(恰好是一个目标)情况的机制。
【解决方案3】:

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

查看Joel Pobar的博客。

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 这一直是我的假设,直到我遇到问题中描述的异常。也许正如乔恩所说的那样,问题出在其他地方,我错误地陷入了“代表速度较慢”的模因。
    • 如果在 SO 上有更多像这样的真正技术答案,展示底层实现是如何实现的,而不是期望提问者盲目相信“它就是这样”。
    【解决方案5】:

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

    这是一个非常粗略的基准测试,它显示(无论如何在我的盒子上)委托实际上比接口

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

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

    【讨论】:

    • 你能详细说明编译器在最后一种情况下做了什么吗?它是在每次迭代时创建一个新的委托实例还是?
    • 如果您使用委托将其转换为事件,这种情况会改变吗?
    • 谢谢乔恩,我不认为这是过多的对象/垃圾收集,但你的基准测试清楚地表明代表至少一样快,所以无论最初的原因是什么,我都会修补我的内部知识这些结果;)
    • 性能怎么样? Action/Func 被实现为委托。委托在 IL 中实现为带有 Invoke() 方法的编译器生成的类。当 foo 是委托时调用 foo() 实际上编译为调用 foo.Invoke(),而后者又调用目标代码。如果 foo 是实际方法而不是委托,则调用 foo() 会直接调用目标代码,而无需 Invoke() 中间。证明见 ILDASM。 stackoverflow.com/a/8449833/206730
    • @Kiquenet:如果您同时使用接口或虚拟方法,也会引入额外的间接级别。是的,如果你直接调用一个非虚拟方法,你可以获得稍微更好的性能,但根据我的经验,它实际上并不重要。
    猜你喜欢
    • 1970-01-01
    • 2015-05-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多