Wydajność wywołania delegatów a metody

Podążając za tym pytaniem - Podaj metodę jako parametr przy użyciu C# i niektóre z moich osobistych doświadczeń chciałbym dowiedzieć się trochę więcej o wydajności wywoływania delegata zamiast tylko wywoływania metody w C#.

Chociaż delegaci są bardzo wygodne, miałem aplikację, która zrobiła wiele wywołań zwrotnych za pośrednictwem delegatów i kiedy przepisać to do korzystania z interfejsów zwrotnych mamy rząd wielkości poprawy prędkości. To było z. NET 2.0 więc nie jestem pewien, jak rzeczy się zmieniły z 3 i 4.

W jaki sposób wywołania delegatów są obsługiwane wewnętrznie w kompilatorze / CLR i jak wpływa to na wydajność wywołań metod?


EDIT - aby wyjaśnić, co mam na myśli przez delegatów vs interfejsy callback.

Dla połączeń asynchronicznych moja klasa może dostarczyć Zdarzenie OnComplete i powiązanego delegata, do którego dzwoniący mógłby się zapisać.

Alternatywnie mógłbym stworzyć interfejs ICallback z metodą OnComplete, którą implementuje wywołujący a następnie rejestruje się z klasą, która następnie wywoła tę metodę po zakończeniu (tzn. sposób, w jaki Java obsługuje te rzeczy).

Author: Community, 2010-01-18

5 answers

Nie widziałem takiego efektu - z pewnością nigdy nie spotkałem się z tym, że jest wąskim gardłem.

Oto bardzo rough-and-ready benchmark, który pokazuje (i tak na moim pudełku), że delegaty są faktycznie szybsze niż interfejsy:

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);
    }
}
Wyniki (. NET 3.5;. NET 4. 0b2 to mniej więcej to samo):
Interface: 5068
Delegate: 4404

Teraz nie mam szczególnej wiary, że oznacza to, że delegaci sąnaprawdę szybsi niż interfejsy... ale utwierdza mnie to w przekonaniu, że nie są o rząd wielkości wolniej. Dodatkowo, nie robi to prawie nic w metodzie delegate / interface. Oczywiście koszt wywołania będzie coraz mniej różnicy, ponieważ wykonujesz coraz więcej pracy na połączenie.

Jedną rzeczą, na którą należy uważać, jest to, że nie tworzysz nowego delegata kilka razy, gdzie używasz tylko jednej instancji interfejsu. To może spowodować problem, ponieważ sprowokowałoby zbieranie śmieci itp. Jeśli używasz metody instancji jako delegata w pętli, będziesz bardziej wydajne jest deklarowanie zmiennej delegate poza pętlą, tworzenie pojedynczej instancji delegata i ponowne jej użycie. Na przykład:

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

Jest bardziej wydajny niż:

for (int i = 0; i < 100000; i++)
{
    MethodTakingFunc(myInstance.MyMethod);
}

Czy to mógł być problem, który widziałeś?

 69
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-17 22:19:59

Od CLR v 2 koszt wywołania delegata jest bardzo zbliżony do kosztu wywołania metody wirtualnej, która jest używana dla metod interfejsu.

Zobacz Blog Joela Pobara .

 19
Author: Pete Montgomery,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-18 10:45:47

Uważam za całkowicie nieprawdopodobne, że delegat jest znacznie szybszy lub wolniejszy niż metoda wirtualna. Jeśli już, delegat powinien być niedbale szybszy. Na niższym poziomie delegaty są zwykle implementowane w podobny sposób (używając notacji w stylu C, ale proszę wybaczyć drobne błędy składniowe, ponieważ jest to tylko ilustracja): {]}

struct Delegate {
    void* contextPointer;   // What class instance does this reference?
    void* functionPointer;  // What method does this reference?
}

Wywołanie delegata działa tak:

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

Klasa, przetłumaczona na C, byłaby czymś w stylu:

struct SomeClass {
    void** vtable;        // Array of pointers to functions.
    SomeType someMember;  // Member variables.
}

Aby wywołać funkcja vritual, wykonałbyś następujące czynności:

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

Są zasadniczo takie same, z tym wyjątkiem, że podczas korzystania z funkcji wirtualnych przechodzisz przez dodatkową warstwę indrection, aby uzyskać wskaźnik funkcji. Jednak ta dodatkowa warstwa indrection jest często Darmowa, ponieważ nowoczesne predyktory gałęzi procesora odgadną adres wskaźnika funkcji i spekulacyjnie wykonają jego cel równolegle z wyszukaniem adresu funkcji. Znalazłem (choć w D, A Nie w C#) tę wirtualną funkcję wywołania w ciasnej pętli nie są wolniejsze niż wywołania bezpośrednie nieinlinowane, pod warunkiem, że dla dowolnego przebiegu pętli są zawsze rozdzielane do tej samej rzeczywistej funkcji.

 17
Author: dsimcha,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-17 22:57:57

Zrobiłem kilka testów (w. Net 3.5... później sprawdzę w domu za pomocą. Net 4). Faktem jest: Uzyskanie obiektu jako interfejsu, a następnie wykonanie metody jest szybsze niż uzyskanie delegata z metody, a następnie wywołanie delegata.

Biorąc pod uwagę, że zmienna jest już we właściwym typie (interfejs lub delegat) i proste wywołanie powoduje, że delegat wygrywa.

Z jakiegoś powodu uzyskanie delegata nad metodą interfejsu (może nad dowolną metodą wirtualną) jest dużo wolniej.

I biorąc pod uwagę przypadki, w których po prostu nie możemy wstępnie zapisać delegata (jak na przykład w Dyspozytorach), może to uzasadniać, dlaczego interfejsy są szybsze.

Oto wyniki:

Aby uzyskać prawdziwe wyniki, skompiluj to w trybie Release i uruchom poza Visual Studio.

Sprawdzanie połączeń bezpośrednich dwa razy
00:00:00.5834988
00:00:00.5997071

Sprawdzanie połączeń interfejsu, pobieranie interfejsu przy każdym połączeniu
00:00:05.8998212

Sprawdzanie połączeń interfejsu, uzyskanie interfejsu raz
00:00:05.3163224

Sprawdzanie wywołań akcji (delegata), pobieranie akcji przy każdym wywołaniu
00:00:17.1807980

Sprawdzanie wywołań akcji (delegata), uzyskanie akcji raz
00:00:05.3163224

Sprawdzanie akcji (delegata) nad metodą interfejsu, uzyskanie obu w każde wywołanie
00:03:50.7326056

Sprawdzanie akcji (delegata) przez interfejs metoda, uzyskanie interfejs raz, delegat przy każdym wywołaniu
00:03:48.9141438

Sprawdzanie akcji (delegata) przez metodę interfejsu, uzyskanie obu raz
00:00:04.0036530

Jak widzisz, bezpośrednie połączenia są naprawdę szybkie. Przechowywanie interfejsu lub delegata przed, a następnie tylko wywołanie go jest naprawdę szybkie. Ale posiadanie delegata jest wolniejsze niż posiadanie interfejsu. Konieczności uzyskania delegata nad metodą interfejsu (lub metodą wirtualną, nie oczywiście) jest bardzo powolny (Porównaj 5 sekund uzyskania obiektu jako interfejsu do prawie 4 minut robienia tego samego, aby uzyskać akcję).

Kod, który wygenerował te wyniki jest tutaj:

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
Author: Paulo Zemek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-29 10:16:52

A co z tym, że delegaci są kontenerami? Czy możliwość multicastu nie zwiększa kosztów? Skoro już jesteśmy w temacie, co zrobić, jeśli przesuniemy ten aspekt kontenera nieco dalej? Nic nie zabrania nam, Jeśli D Jest delegatem, wykonywania d + = d; lub budowania arbitralnie złożonego ukierunkowanego grafu par (wskaźnik kontekstowy, wskaźnik metody). Gdzie mogę znaleźć dokumentację opisującą jak ten wykres jest przesuwany po wywołaniu delegata?

 1
Author: Dorian Yeager,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-29 14:27:13