Partilhar via


Guia para iniciantes sobre como otimizar o código e reduzir os custos de computação (C#, Visual Basic, C++ e F#)

Reduzir seu tempo de computação significa reduzir custos, portanto, otimizar seu código pode economizar dinheiro. Neste estudo de caso, mostramos como você pode usar ferramentas de criação de perfil para ajudar a realizar essa tarefa.

Nosso objetivo é equipar os desenvolvedores com o conhecimento para:

  • Entender a importância da otimização de código e o impacto que ela tem na redução dos custos de computação.
  • Utilizar as ferramentas de criação de perfil do Visual Studio para analisar o desempenho do aplicativo.
  • Interpretar os dados fornecidos por essas ferramentas para identificar gargalos de desempenho.
  • Aplicar estratégias práticas para otimizar códigos, com foco no uso da CPU, alocação de memória e interações com o banco de dados.

Ao final deste guia, os leitores deverão ser capazes de aplicar essas técnicas a seus próprios projetos, obtendo assim aplicativos mais eficientes e econômicos.

Estudo de caso de otimização

O aplicativo de exemplo discutido neste estudo de caso é um aplicativo .NET projetado para executar consultas em um banco de dados de blogs e postagens de blog associadas. Ele utiliza o Entity Framework, um ORM (Mapeamento Relacional de Objeto) para .NET conhecido, para interagir com um banco de dados local SQLite. O aplicativo é estruturado para executar um número grande de consultas, simulando um cenário do mundo real em que um aplicativo .NET precise lidar com tarefas extensas de recuperação de dados. O aplicativo de exemplo é uma versão modificada do exemplo do Entity Framework.

O principal problema de desempenho com o aplicativo de exemplo está na forma como ele gerencia recursos de computação e interage com o banco de dados. O aplicativo sofre de um gargalo de desempenho comum que afeta significativamente sua eficiência e, consequentemente, os custos de computação associados à sua execução. O problema inclui os sintomas a seguir:

  • Alto uso da CPU: os aplicativos podem executar computações ou tarefas de processamento ineficientes de uma forma que consome uma quantidade desnecessariamente grande de recursos da CPU. Isso pode levar a tempos de resposta lentos e aumento dos custos operacionais.

  • Alocação de memória ineficiente: às vezes, os aplicativos podem ter problemas relacionados ao uso e alocação de memória. Em aplicativos .NET, o gerenciamento de memória ineficiente pode levar ao aumento da coleta de lixo, e isso pode afetar o desempenho do aplicativo.

  • Sobrecarga de interação de banco de dados: aplicativos que executam um grande número de consultas em um banco de dados podem enfrentar gargalos relacionados a interações de banco de dados. Isso inclui consultas ineficientes, chamadas excessivas de banco de dados e uso insatisfatório dos recursos do Entity Framework, e tudo isso pode prejudicar o desempenho.

O estudo de caso visa abordar esses problemas utilizando as ferramentas de criação de perfil do Visual Studio para analisar o desempenho do aplicativo. Ao entender onde e como o desempenho do aplicativo pode ser melhorado, os desenvolvedores podem implementar otimizações para reduzir o uso da CPU, melhorar a eficiência da alocação de memória, simplificar as interações com o banco de dados e otimizar a utilização de recursos. O objetivo final é melhorar o desempenho geral do aplicativo, tornando-o mais eficiente e econômico para execução.

Desafio

Há vários desafios para resolver os problemas de desempenho no aplicativo .NET de exemplo. Esses desafios decorrem da complexidade do diagnóstico de gargalos de desempenho. Os principais desafios na correção dos problemas descritos são:

  • Diagnosticar gargalos de desempenho: um dos principais desafios é identificar com precisão as causas básicas dos problemas de desempenho. O alto uso da CPU, a alocação ineficiente de memória e as despesas gerais de interação com o banco de dados podem ter vários fatores contribuintes. Os desenvolvedores devem usar ferramentas de criação de perfil de forma eficaz para diagnosticar esses problemas, o que requer alguma compreensão de como essas ferramentas funcionam e como interpretar sua saída.

  • Conhecimento e restrições de recursos: finalmente, as equipes podem enfrentar restrições relacionadas ao conhecimento, experiência e recursos. Criar o perfil e otimizar um aplicativo requer habilidades e experiência específicas, e nem todas as equipes podem ter acesso imediato a esses recursos.

Para enfrentar esses desafios, é necessária uma abordagem estratégica que combine o uso eficaz de ferramentas de criação de perfil, conhecimento técnico e planejamento e testes cuidadosos. O estudo de caso tem como objetivo orientar os desenvolvedores nesse processo, fornecendo estratégias e insights para superar esses desafios e melhorar o desempenho do aplicativo.

Estratégia

Esta é uma visão de alto nível da abordagem:

  • Iniciamos a investigação fazendo um rastreamento de uso da CPU. A Ferramenta de uso da CPU do Visual Studio costuma ser útil para iniciar investigações de desempenho e otimizar o código para reduzir custos.
  • Em seguida, para obter informações adicionais e ajudar a isolar problemas ou melhorar o desempenho, coletamos um rastreamento usando uma das outras ferramentas de criação de perfil. Por exemplo:
    • Vamos analisar o uso de memória. Para o .NET, experimentamos a ferramenta de alocação de objetos .NET primeiro. (Alternativamente, para .NET ou C++, você pode conferir a ferramenta Uso de memória.)
    • Para ADO.NET ou Entity Framework, podemos usar a ferramenta de Banco de Dados para examinar consultas SQL, tempo de consulta preciso e muito mais.

A coleta de dados requer as seguintes etapas:

  • Definimos o aplicativo para uma compilação de versão.
  • Selecionamos a ferramenta Uso da CPU no Criador de Perfil de Desempenho (Alt+F2). (As etapas posteriores envolvem algumas das outras ferramentas.)
  • No Criador de Perfil de Desempenho, iniciamos o aplicativo e coletamos um rastreamento.

Inspecionar áreas de alto uso da CPU

Depois de coletar um rastreamento com a ferramenta Uso da CPU e carregá-lo no Visual Studio, primeiro verificamos a página de relatório inicial .diagsession que mostra os Principais Insights e o Caminho Crítico. O Caminho Crítico mostra o caminho de código com maior uso de CPU no aplicativo. Estas seções podem fornecer dicas e nos ajudar a identificar rapidamente problemas de desempenho que podemos melhorar.

Também podemos visualizar o caminho ativo na visualização Árvore de Chamadas. Para abrir esse modo de exibição, use o link Abrir detalhes no relatório e selecione Árvore de Chamadas.

Nessa exibição, vemos o caminho crítico novamente, que mostra o alto uso da CPU para o método GetBlogTitleX no aplicativo, com cerca de 60% do uso da CPU do aplicativo. No entanto, o valor de CPU própria para GetBlogTitleX é baixo, apenas cerca de 0,10%. Ao contrário do valor de CPU total, o valor de CPU Própria exclui o tempo gasto em outras funções, portanto, devemos procurar pelo o gargalo real mais para baixo no modo de exibição de árvore de chamadas.

Captura de tela da visualização da árvore de chamadas na ferramenta de Uso da CPU.

GetBlogTitleX faz chamadas externas para duas DLLs LINQ, que estão usando a maior parte do tempo de CPU, conforme evidenciado pelos valores muito altos de CPU própria. Esta é a primeira pista de que pode ser interessante procurar uma consulta LINQ como uma área para otimizar.

Captura de tela da exibição Árvore de Chamadas na ferramenta Uso da CPU com a CPU Própria em destaque.

Para obter uma árvore de chamadas visualizada e uma exibição diferente dos dados, clique com o botão direito do mouse em GetBlogTitleX e escolha Exibir no Flame Graph. Novamente, parece que o método GetBlogTitleX é responsável por grande parte do uso da CPU do aplicativo (mostrado em amarelo). As chamadas externas para as DLLs do LINQ aparecem abaixo da caixa GetBlogTitleX e estão usando todo o tempo de CPU para o método.

Captura de tela da exibição do Gráfico de Chamas na ferramenta de uso da CPU.

Reunir dados adicionais

Muitas vezes, outras ferramentas podem fornecer informações adicionais para ajudar a análise e isolar o problema. Para este exemplo, adotamos a seguinte abordagem:

  • Primeiro, vamos analisar o uso de memória. Pode haver uma correlação entre o alto uso da CPU e o alto uso de memória. Por isso, pode ser útil examinar ambos para isolar o problema.
  • Como identificamos as DLLs do LINQ, também examinaremos a ferramenta de banco de dados.

Verificar o uso de memória

Para ver o que está acontecendo com o aplicativo em termos de uso de memória, coletamos um rastreamento usando a ferramenta Alocação de Objetos .NET (no C++, você pode usar a ferramenta Uso de Memória). O modo de exibição Árvore de Chamadas no rastreamento de memória mostra o caminho crítico e nos ajuda a identificar uma área de alto uso de memória. Não será surpresa neste momento que o método GetBlogTitleX parece estar gerando muitos objetos! Mais de 900.000 alocações de objeto, na verdade.

Captura de tela da exibição da Árvore de Chamadas na ferramenta de Alocação de Objeto .NET.

A maioria dos objetos criados é de sequências, matrizes de objetos e Int32s. Podemos ver como esses tipos são gerados examinando o código-fonte.

Verificar a consulta na ferramenta de banco de dados

No Criador de Perfil de Desempenho, selecionamos a ferramenta Banco de Dados em vez de Uso da CPU (ou podemos selecionar ambos). Depois de coletar um rastreamento, selecione a guia Consultas na página de diagnóstico. Na guia Consultas para o rastreamento do Banco de dados, podemos ver que a primeira linha mostra a consulta mais longa, de 2446 ms. A coluna Registros mostra quantos registros a consulta lê. Podemos usar essas informações para comparação posterior.

Captura de tela das consultas de banco de dados na ferramenta Banco de Dados.

Ao examinar a instrução SELECT gerada pelo LINQ na coluna Consulta, identificamos a primeira linha como a consulta associada ao método GetBlogTitleX. Para exibir a cadeia de caracteres de consulta completa, expandimos a largura da coluna. A cadeia de caracteres de consulta completa é:

SELECT "b"."Url", "b"."BlogId", "p"."PostId", "p"."Author", "p"."BlogId", "p"."Content", "p"."Date", "p"."MetaData", "p"."Title"
FROM "Blogs" AS "b" LEFT JOIN "Posts" AS "p" ON "b"."BlogId" = "p"."BlogId" ORDER BY "b"."BlogId"

Observe que estamos recuperando muitos valores de coluna aqui, talvez até mais do que precisamos. Vejamos o código-fonte.

Otimizar código

É hora de dar uma olhada no código-fonte GetBlogTitleX. Na ferramenta Banco de Dados, clicamos com o botão direito do mouse na consulta e escolhemos Ir para o Arquivo de Origem. No código-fonte de GetBlogTitleX, encontramos o código a seguir que usa LINQ para ler o banco de dados.

foreach (var blog in db.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
  {
    foreach (var post in blog.Posts)
    {
      if (post.Author == "Fred Smith")
      {
        Console.WriteLine($"Post: {post.Title}");
      }
  }
}

Esse código usa loops foreach para pesquisar no banco de dados qualquer blog com "Fred Smith" como autor. Em análise, podemos ver que muitos objetos estão sendo gerados na memória: uma nova matriz de objetos para cada blog no banco de dados, cadeias de caracteres associadas a cada URL e valores para propriedades contidas nas postagens, como ID do blog.

Fazemos uma pesquisa rápida e encontramos algumas recomendações comuns sobre como otimizar consultas LINQ e criar esse código.

foreach (var x in db.Posts.Where(p => p.Author.Contains("Fred Smith")).Select(b => b.Title).ToList())
{
  Console.WriteLine("Post: " + x);
}

Neste código, fizemos várias alterações para ajudar a otimizar a consulta:

  • Adicionou a cláusula Where e eliminou um dos loops foreach.
  • Projetamos apenas a propriedade Title na instrução Select, que é o necessário neste exemplo.

Em seguida, testamos novamente usando as ferramentas de criação de perfil.

Resultados

Depois de atualizar o código, executamos novamente a ferramenta Uso da CPU para coletar um rastreamento. O modo de exibição de Árvore de Chamadas mostra que GetBlogTitleX está executando apenas 1754 ms, usando 37% do total de CPU do aplicativo, um aprimoramento significativo de 59%.

Captura de tela do uso aprimorado da CPU na visualização de Árvore de Chamadas da ferramenta de Uso da CPU.

Alternamos para a visualização Flame Graph para conferir outra visualização da melhoria. Nessa exibição, GetBlogTitleX também usa uma parte menor da CPU.

Captura de tela do uso aprimorado da CPU na visualização Gráfico de Chama da ferramenta de Uso da CPU.

Verificamos os resultados no rastreamento da ferramenta Banco de dados, e apenas dois registros são lidos usando essa consulta, em vez de 100.000! Além disso, a consulta é muito simplificada e elimina a LEFT JOIN desnecessária que foi gerada anteriormente.

Captura de tela do tempo de consulta mais rápido na ferramenta Banco de Dados.

Em seguida, verificamos novamente os resultados na ferramenta Alocação de objetos .NET e vemos que GetBlogTitleX é responsável por apenas 56.000 alocações de objetos, uma redução de quase 95% em relação a 900.000!

Captura de tela das alocações de memória reduzidas na ferramenta de alocação de objeto .NET.

ITERAR

Podem ser necessárias várias otimizações e podemos continuar a iterar com alterações de código para ver quais alterações melhoram o desempenho e reduzem nosso custo de computação.

Próximas etapas

Os artigos e as postagens no blog apresentados a seguir fornecem mais informações para ajudar você a aprender a usar as ferramentas de desempenho do Visual Studio de maneira efetiva.