Eu tenho uma pergunta sobre o desempenho do dynamic
em c #. Eu li que dynamic
faz o compilador funcionar novamente, mas o que ele faz?
Ele precisa recompilar todo o método com a dynamic
variável usada como parâmetro ou apenas as linhas com comportamento / contexto dinâmico?
Notei que o uso de dynamic
variáveis pode retardar um loop for simples em 2 ordens de magnitude.
Código com o qual joguei:
internal class Sum2
{
public int intSum;
}
internal class Sum
{
public dynamic DynSum;
public int intSum;
}
class Program
{
private const int ITERATIONS = 1000000;
static void Main(string[] args)
{
var stopwatch = new Stopwatch();
dynamic param = new Object();
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
Console.ReadKey();
}
private static void Sum(Stopwatch stopwatch)
{
var sum = 0;
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch, dynamic param)
{
var sum = new Sum2();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
}
private static void DynamicSum(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.DynSum += i;
}
stopwatch.Stop();
Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
c#
performance
dynamic
Lukasz Madon
fonte
fonte
Respostas:
Aqui está o acordo.
Para cada expressão no seu programa que é do tipo dinâmico, o compilador emite código que gera um único "objeto de site de chamada dinâmica" que representa a operação. Então, por exemplo, se você tiver:
o compilador gerará um código moralmente semelhante a este. (O código real é um pouco mais complexo; isso é simplificado para fins de apresentação.)
Veja como isso funciona até agora? Geramos o site de chamada uma vez , não importa quantas vezes você chame M. O site de chamada permanece para sempre depois que você o gera uma vez. O site de chamada é um objeto que representa "haverá uma chamada dinâmica para Foo aqui".
OK, agora que você recebeu o site de chamada, como funciona a chamada?
O site de chamada faz parte do Dynamic Language Runtime. O DLR diz "hmm, alguém está tentando fazer uma invocação dinâmica de um método para esse objeto aqui. Eu sei alguma coisa sobre isso? Não. Então é melhor descobrir."
O DLR, em seguida, interroga o objeto em d1 para ver se é algo especial. Talvez seja um objeto COM herdado, ou um objeto Iron Python, ou um objeto Iron Ruby, ou um objeto DOM do IE. Se não for um desses, deve ser um objeto C # comum.
Este é o ponto em que o compilador é iniciado novamente. Não há necessidade de um lexer ou analisador; portanto, o DLR inicia uma versão especial do compilador C # que possui apenas o analisador de metadados, o analisador semântico para expressões e um emissor que emite Árvores de expressão em vez de IL.
O analisador de metadados usa o Reflection para determinar o tipo do objeto em d1 e depois o passa para o analisador semântico para perguntar o que acontece quando esse objeto é invocado no método Foo. O analisador de resolução de sobrecarga descobre isso e cria uma árvore de expressão - como se você tivesse chamado Foo em uma árvore de expressão lambda - que representa essa chamada.
O compilador C # então passa essa árvore de expressão de volta para o DLR junto com uma política de cache. A política geralmente é "a segunda vez que você vê um objeto desse tipo, pode reutilizar essa árvore de expressão em vez de me ligar de volta". O DLR chama Compile na árvore de expressão, que invoca o compilador expressão-árvore-para-IL e cospe um bloco de IL gerada dinamicamente em um delegado.
O DLR armazena em cache esse delegado em um cache associado ao objeto do site de chamada.
Em seguida, ele invoca o delegado e a chamada Foo acontece.
Na segunda vez em que você ligar para M, já temos um site de chamadas. O DLR interroga o objeto novamente e, se o objeto for do mesmo tipo da última vez, buscará o delegado no cache e o chamará. Se o objeto é de um tipo diferente, o cache falha e todo o processo é iniciado novamente; fazemos análise semântica da chamada e armazenamos o resultado no cache.
Isso acontece para todas as expressões que envolvem dinâmico. Então, por exemplo, se você tiver:
existem três sites de chamadas dinâmicas. Um para a chamada dinâmica para Foo, um para a adição dinâmica e outro para a conversão dinâmica de dinâmico para int. Cada um tem sua própria análise de tempo de execução e seu próprio cache de resultados de análise.
Faz sentido?
fonte
Atualização: Adicionados benchmarks pré-compilados e preguiçosos
Atualização 2: Acontece que eu estou errado. Veja a publicação de Eric Lippert para obter uma resposta completa e correta. Estou deixando isso aqui por causa dos números de referência
* Atualização 3: Adicionados parâmetros de referência emitidos por IL e preguiçosos emitidos por IL, com base na resposta de Mark Gravell a esta pergunta .
Que eu saiba, o uso dadynamic
palavra - chave não causa nenhuma compilação extra em tempo de execução por si só (embora eu imagine que poderia fazê-lo em circunstâncias específicas, dependendo do tipo de objeto que está apoiando suas variáveis dinâmicas).Em relação ao desempenho,
dynamic
introduz inerentemente alguma sobrecarga, mas não tanto quanto você imagina. Por exemplo, eu apenas executei uma referência parecida com esta:Como você pode ver no código, tento invocar um método simples não operacional de sete maneiras diferentes:
dynamic
Action
que foi pré-compilado em tempo de execução (excluindo assim o tempo de compilação dos resultados).Action
que é compilado na primeira vez em que é necessário, usando uma variável Lazy não segura para thread (incluindo o tempo de compilação)Cada um é chamado 1 milhão de vezes em um loop simples. Aqui estão os resultados do tempo:
Portanto, enquanto o uso da
dynamic
palavra-chave leva uma ordem de magnitude maior que a chamada direta do método, ele ainda consegue concluir a operação um milhão de vezes em cerca de 50 milissegundos, tornando-a muito mais rápida que a reflexão. Se o método que chamamos estava tentando fazer algo intensivo, como combinar algumas cadeias ou procurar um valor em uma coleção, essas operações provavelmente superariam em muito a diferença entre uma chamada direta e umadynamic
chamada.O desempenho é apenas uma das muitas boas razões para não usar
dynamic
desnecessariamente, mas quando você lida comdynamic
dados reais , pode oferecer vantagens que superam as desvantagens.Atualização 4
Baseado no comentário de Johnbot, dividi a área de reflexão em quatro testes separados:
... e aqui estão os resultados de referência:
Portanto, se você pode predeterminar um método específico que precisará chamar muito, chamar um delegado em cache que se refere a esse método é tão rápido quanto chamar o próprio método. No entanto, se você precisar determinar qual método chamar, assim que estiver prestes a invocá-lo, a criação de um representante para ele será muito cara.
fonte
dynamic
obviamente perdendo:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);