Freqüentemente, no SO, eu me pego comparando pequenos pedaços de código para ver qual implementação é mais rápida.
Frequentemente vejo comentários de que o código de benchmarking não leva em conta o jitting ou o coletor de lixo.
Tenho a seguinte função de benchmarking simples, que evoluí lentamente:
static void Profile(string description, int iterations, Action func) {
// warm up
func();
// clean up
GC.Collect();
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}
Uso:
Profile("a descriptions", how_many_iterations_to_run, () =>
{
// ... code being profiled
});
Esta implementação tem alguma falha? É bom o suficiente para mostrar que a implementação X é mais rápida do que a implementação Y em iterações Z? Você pode pensar em alguma maneira de melhorar isso?
EDITAR É bastante claro que uma abordagem baseada no tempo (em oposição às iterações) é preferida. Alguém tem alguma implementação em que as verificações de tempo não afetam o desempenho?
c#
.net
performance
profiling
Sam Saffron
fonte
fonte
Respostas:
Aqui está a função modificada: conforme recomendado pela comunidade, sinta-se à vontade para alterá-la, é uma wiki da comunidade.
Certifique-se de compilar na versão com otimizações habilitadas e execute os testes fora do Visual Studio . Esta última parte é importante porque o JIT restringe suas otimizações com um depurador anexado, mesmo no modo Release.
fonte
A finalização não será necessariamente concluída antes do
GC.Collect
retorno. A finalização é enfileirada e, em seguida, executada em um thread separado. Este thread ainda pode estar ativo durante seus testes, afetando os resultados.Se você deseja garantir que a finalização foi concluída antes de iniciar seus testes, convém chamar
GC.WaitForPendingFinalizers
, que será bloqueado até que a fila de finalização seja limpa:fonte
GC.Collect()
que mais uma vez?Collect
está lá para garantir que os objetos "finalizados" também sejam coletados.Se você quiser tirar as interações do GC da equação, pode executar sua chamada de 'aquecimento' após a chamada GC.Collect, não antes. Dessa forma, você saberá que o .NET já terá memória suficiente alocada do sistema operacional para o conjunto de trabalho de sua função.
Lembre-se de que você está fazendo uma chamada de método não sequencial para cada iteração, portanto, certifique-se de comparar as coisas que está testando com um corpo vazio. Você também terá que aceitar que só pode cronometrar com segurança coisas que são várias vezes mais longas do que uma chamada de método.
Além disso, dependendo do tipo de coisa que você está traçando, você pode querer fazer sua execução com base no tempo por um certo período de tempo, em vez de por um certo número de iterações - isso pode levar a números mais facilmente comparáveis sem ter que ter um prazo muito curto para a melhor implementação e / ou um prazo muito longo para a pior.
fonte
Eu evitaria passar pelo delegado:
Um exemplo de código que leva ao uso de fechamento:
Se você não está ciente dos fechamentos, dê uma olhada neste método no .NET Reflector.
fonte
IDisposable
.Acho que o problema mais difícil de superar com métodos de benchmarking como esse é levar em conta os casos extremos e o inesperado. Por exemplo - "Como os dois trechos de código funcionam sob alta carga de CPU / uso de rede / fragmentação de disco / etc." Eles são ótimos para verificações lógicas básicas para ver se um algoritmo específico funciona significativamente mais rápido do que outro. Mas, para testar adequadamente a maior parte do desempenho do código, você teria que criar um teste que meça os gargalos específicos desse código em particular.
Eu ainda diria que testar pequenos blocos de código geralmente tem pouco retorno sobre o investimento e pode encorajar o uso de código excessivamente complexo em vez de código simples de manutenção. Escrever um código claro que outros desenvolvedores, ou eu mesmo 6 meses depois, possamos entender rapidamente terá mais benefícios de desempenho do que um código altamente otimizado.
fonte
Eu ligaria
func()
várias vezes para o aquecimento, não apenas uma.fonte
Sugestões para melhorar
Detectar se o ambiente de execução é bom para benchmarking (como detectar se um depurador está conectado ou se a otimização jit está desabilitada, o que resultaria em medições incorretas).
Medir partes do código de forma independente (para ver exatamente onde está o gargalo).
Em relação ao nº 1:
Para detectar se um depurador está anexado, leia a propriedade
System.Diagnostics.Debugger.IsAttached
(lembre-se de lidar também com o caso em que o depurador inicialmente não está anexado, mas é anexado depois de algum tempo).Para detectar se a otimização jit está desabilitada, leia a propriedade
DebuggableAttribute.IsJITOptimizerDisabled
dos conjuntos relevantes:Em relação ao nº 2:
Isso pode ser feito de várias maneiras. Uma maneira é permitir que vários delegados sejam fornecidos e então medir esses delegados individualmente.
Em relação ao nº 3:
Isso também poderia ser feito de várias maneiras, e diferentes casos de uso exigiriam soluções muito diferentes. Se o benchmark for invocado manualmente, pode ser bom gravar no console. No entanto, se o benchmark for executado automaticamente pelo sistema de compilação, provavelmente não será tão bom gravar no console.
Uma maneira de fazer isso é retornar o resultado do benchmark como um objeto fortemente tipado que pode ser facilmente consumido em diferentes contextos.
Etimo.Benchmarks
Outra abordagem é usar um componente existente para realizar os benchmarks. Na verdade, na minha empresa, decidimos lançar nossa ferramenta de benchmark para domínio público. Em seu núcleo, ele gerencia o coletor de lixo, jitter, aquecimentos etc, assim como algumas das outras respostas aqui sugerem. Ele também tem os três recursos que sugeri acima. Ele gerencia vários dos problemas discutidos no blog de Eric Lippert .
Este é um exemplo de saída em que dois componentes são comparados e os resultados são gravados no console. Neste caso, os dois componentes comparados são chamados de 'KeyedCollection' e 'MultiplyIndexedKeyedCollection':
Há um pacote NuGet , um pacote NuGet de amostra e o código-fonte está disponível no GitHub . Há também uma postagem no blog .
Se você estiver com pressa, sugiro que pegue o pacote de amostra e simplesmente modifique os representantes de amostra conforme necessário. Se você não estiver com pressa, pode ser uma boa ideia ler a postagem do blog para entender os detalhes.
fonte
Você também deve executar uma passagem de "aquecimento" antes da medição real para excluir o tempo que o compilador JIT gasta para ajustar seu código.
fonte
Dependendo do código que você está avaliando e da plataforma em que ele é executado, pode ser necessário considerar como o alinhamento do código afeta o desempenho . Para fazer isso, provavelmente seria necessário um wrapper externo que executasse o teste várias vezes (em domínios ou processos de aplicativos separados?), Algumas das vezes chamando primeiro o "código de preenchimento" para forçá-lo a ser compilado por JIT, de modo a fazer com que o código seja avaliado para ser alinhado de forma diferente. Um resultado de teste completo forneceria os tempos de melhor e pior caso para os vários alinhamentos de código.
fonte
Se você está tentando eliminar o impacto da coleta de lixo do benchmark completo, vale a pena defini-lo
GCSettings.LatencyMode
?Se não, e você deseja que o impacto do lixo criado em faça
func
parte do benchmark, então não deveria também forçar a coleta no final do teste (dentro do cronômetro)?fonte
O problema básico com sua pergunta é a suposição de que uma única medição pode responder a todas as suas perguntas. Você precisa medir várias vezes para obter uma imagem eficaz da situação e, especialmente, em uma linguagem de coleta de lixo como C #.
Outra resposta fornece uma maneira correta de medir o desempenho básico.
No entanto, essa medição única não leva em consideração a coleta de lixo. Um perfil adequado também é responsável pelo pior caso de desempenho da coleta de lixo espalhada por muitas chamadas (esse número é meio inútil, pois a VM pode terminar sem nunca coletar o lixo restante, mas ainda é útil para comparar duas implementações diferentes de
func
).E alguém também pode querer medir o desempenho do pior caso de coleta de lixo para um método que é chamado apenas uma vez.
Porém, mais importante do que recomendar quaisquer medidas adicionais específicas possíveis para o perfil é a ideia de que se deve medir várias estatísticas diferentes e não apenas um tipo de estatística.
fonte