Desempenho de chamar delegates versus methods

Após essa pergunta – Passar Método como Parâmetro usando C # e algumas das minhas experiências pessoais eu gostaria de saber um pouco mais sobre o desempenho de chamar um delegado vs apenas chamando um método em C #.

Embora os delegates sejam extremamente convenientes, eu tinha um aplicativo que fazia muitos callbacks via delegates e quando reescrevemos isso para usar interfaces de retorno de chamada, obtivemos uma melhoria de velocidade de ordem de grandeza. Isso foi com o .NET 2.0, então não tenho certeza de como as coisas mudaram com 3 e 4.

Como as chamadas para os delegates são tratadas internamente no compilador / CLR e como isso afeta o desempenho das chamadas de método?


EDIT – Para esclarecer o que quero dizer por delegates vs interfaces de retorno de chamada.

Para chamadas assíncronas, minha class poderia fornecer um evento OnComplete e um delegado associado aos quais o chamador poderia se inscrever.

Alternativamente, eu poderia criar uma interface ICallback com um método OnComplete que o chamador implementa e, em seguida, registra-se com a class que, em seguida, chamará esse método na conclusão (ou seja, a maneira como Java lida com essas coisas).

Eu não vi esse efeito – eu certamente nunca encontrei um gargalo.

Aqui está um benchmark muito duro e pronto que mostra (na minha checkbox de qualquer maneira) delegates sendo realmente mais rápidos do que interfaces:

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

Resultados (.NET 3.5; .NET 4.0b2 é aproximadamente o mesmo):

 Interface: 5068 Delegate: 4404 

Agora eu não tenho uma fé particular de que isso significa que os delegates são realmente mais rápidos do que as interfaces ... mas isso me deixa bastante convencido de que eles não são uma ordem de magnitude mais lenta. Além disso, isso está fazendo quase nada dentro do método delegate / interface. Obviamente, o custo de invocação vai fazer cada vez menos diferença à medida que você faz mais e mais trabalho por chamada.

Uma coisa a ter cuidado é que você não está criando um novo delegate várias vezes em que você usaria apenas uma única instância de interface. Isso pode causar um problema, pois isso provocaria garbage collection etc. Se você estiver usando um método de instância como um delegado em um loop, você achará mais eficiente declarar a variável de delegado fora do loop, criar uma única instância de delegação e reutilizar isto. Por exemplo:

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

é mais eficiente que:

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

Este poderia ter sido o problema que você estava vendo?

Como CLR v 2, o custo da chamada de delegação é muito próximo ao da chamada de método virtual, que é usada para methods de interface.

Veja o blog de Joel Pobar .

Eu acho completamente implausível que um delegado seja substancialmente mais rápido ou mais lento que um método virtual. Se alguma coisa o delegado deve ser negligentemente mais rápido. Em um nível mais baixo, os delegates geralmente são implementados como algo (usando a notação em estilo C, mas, por favor, perdoem quaisquer pequenos erros de syntax, pois isso é apenas uma ilustração):

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

Chamar um delegado funciona algo como:

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

Uma class, traduzida para C, seria algo como:

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

Para chamar uma function vritual, você faria o seguinte:

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

Eles são basicamente os mesmos, exceto que ao usar funções virtuais você passa por uma camada extra de indireção para obter o ponteiro de function. No entanto, essa camada extra de indireção geralmente é livre porque os preditores de ramificação da CPU modernos adivinham o endereço do ponteiro de function e executam especulativamente seu destino em paralelo com a procura do endereço da function. Eu encontrei (embora em D, não C #) que chamadas de function virtual em um loop apertado não são mais lentas do que chamadas diretas não-inline, desde que para qualquer execução do loop eles estão sempre resolvendo a mesma function real .

Fiz alguns testes (no .net 3.5 … mais tarde vou verificar em casa usando .net 4). O fato é: Obter um object como uma interface e, em seguida, executar o método é mais rápido do que obter um delegado de um método e chamar o delegado.

Considerando que a variável já está no tipo correto (interface ou delegado) e a simples chamada faz com que o delegado ganhe.

Por algum motivo, obter um delegado sobre um método de interface (talvez sobre qualquer método virtual) é MUITO mais lento.

E, considerando que há casos em que nós simples não podemos pré-armazenar o delegado (como em Dispatches, por exemplo), isso pode justificar porque as interfaces são mais rápidas.

Aqui estão os resultados:

Para obter resultados reais, compilar isso no modo Release e executá-lo fora do Visual Studio.

Verificar as chamadas diretas duas vezes
00: 00: 00.5834988
00: 00: 00.5997071

Verificar as chamadas da interface, obtendo a interface em cada chamada
00: 00: 05.8998212

Verificando as chamadas da interface, obtendo a interface uma vez
00: 00: 05.3163224

Verificar as chamadas de ação (delegado), obtendo a ação em cada chamada
00: 00: 17.1807980

Verificando chamadas de ação (delegadas), obtendo a ação uma vez
00: 00: 05.3163224

Verificando a ação (delegado) sobre um método de interface, obtendo ambos em cada chamada
00: 03: 50.7326056

Ação de verificação (delegado) sobre um método de interface, obtendo a interface uma vez, o delegado em cada chamada
00: 03: 48.9141438

Verificando a ação (delegado) sobre um método de interface, obtendo os dois uma vez
00: 00: 04.0036530

Como você pode ver, as chamadas diretas são muito rápidas. Armazenar a interface ou delegar antes, e apenas chamar é realmente rápido. Mas ter que obter um delegado é mais lento do que ter uma interface. Ter que obter um delegado sobre um método de interface (ou método virtual, não tenho certeza) é muito lento (compare os 5 segundos de obter um object como uma interface para os quase 4 minutos fazendo o mesmo para obter a ação).

O código que gerou esses resultados está aqui:

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

E quanto ao fato de os delegates serem contêineres? A capacidade multicast não adiciona sobrecarga? Enquanto estamos no assunto, e se forçarmos um pouco mais esse aspecto do contêiner? Nada nos proíbe, se d for um delegado, de executar d + = d; ou da construção de um grafo arbitrariamente complexo direcionado de pares (ponteiro de contexto, ponteiro de método). Onde posso encontrar a documentação que descreve como esse gráfico é percorrido quando o delegado é chamado?