调用代理与方法的性能

Pao*_*olo 59 .net c# performance delegates

遵循这个问题 - 使用C#传递方法作为参数和我的一些个人经验我想更多地了解调用委托与仅在C#中调用方法的性能.

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

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


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

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

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

Jon*_*eet 75

我没有看到这种效果 - 我当然从来没有遇到它是一个瓶颈.

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Interface: 5068
Delegate: 4404
Run Code Online (Sandbox Code Playgroud)

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

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

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

比以下更有效:

for (int i = 0; i < 100000; i++)
{
    MethodTakingFunc(myInstance.MyMethod);
}
Run Code Online (Sandbox Code Playgroud)

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

  • 谢谢乔恩,我不认为这是一个过多的对象/垃圾收集,但你的基准测试整齐地表明代表至少同样快,所以不管原始原因我会用这些结果修补我的内部知识;) (4认同)
  • @Kiquenet:如果您正在使用接口或虚拟方法,那么也会引入额外的间接级别.是的,如果你直接调用非虚方法,你可以获得*稍微*更好的性能,但在我的经验中它实际上并不重要. (3认同)
  • 如果您使用委托将其转换为事件,这会改变吗? (2认同)
  • 性能如何?动作/功能被实现为委托。在IL中,委托是使用Invoke()方法作为编译器生成的类实现的。当foo是委托时调用foo()实际上会编译为调用foo.Invoke(),后者依次调用目标代码。如果foo是实际方法而不是委托,则调用foo()会直接调用目标代码,而无需Invoke()中间对象。有关证据,请参见ILDASM。http://stackoverflow.com/a/8449833/206730 (2认同)

Pet*_*ery 21

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

Joel Pobar的博客.


dsi*_*cha 19

我发现委托比虚拟方法更快或更慢是完全不可信的.如果有的话,代表应该可以忽略不计.在较低级别,代理通常实现类似(使用C风格的表示法,但请原谅任何轻微的语法错误,因为这只是一个例子):

struct Delegate {
    void* contextPointer;   // What class instance does this reference?
    void* functionPointer;  // What method does this reference?
}
Run Code Online (Sandbox Code Playgroud)

调用代理的工作方式如下:

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

翻译为C的类将是:

struct SomeClass {
    void** vtable;        // Array of pointers to functions.
    SomeType someMember;  // Member variables.
}
Run Code Online (Sandbox Code Playgroud)

要调用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);
Run Code Online (Sandbox Code Playgroud)

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

  • 如果在SO上只有更多真实的技术答案,展示如何实现基础实现,而不是期望askers依赖盲目信仰"它是如此". (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时获取动作

检查Action(委托)调用,获取Action
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();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 您可能不应该包括在运行它所花费的时间内获取委托. (2认同)