Dicas e truques sobre desempenho em aplicativos .NET

 

Emmanuel Schanzer
Microsoft Corporation

Agosto de 2001

Resumo: Este artigo destina-se a desenvolvedores que desejam ajustar seus aplicativos para obter um desempenho ideal no mundo gerenciado. Códigos de exemplo, explicações e diretrizes de design são abordados para aplicativos de Banco de Dados, Windows Forms e ASP, bem como dicas específicas de linguagem para Microsoft Visual Basic e C++Gerenciado. (25 páginas impressas)

Sumário

Visão geral
Dicas de desempenho para todos os aplicativos
Dicas para acesso ao banco de dados
Dicas de desempenho para aplicativos ASP.NET
Dicas para portabilidade e desenvolvimento no Visual Basic
Dicas para portabilidade e desenvolvimento em C++ gerenciado
Recursos adicionais
Apêndice: custo de chamadas e alocações virtuais

Visão geral

Este white paper foi projetado como uma referência para desenvolvedores que escrevem aplicativos para .NET e procuram várias maneiras de melhorar o desempenho. Se você for um desenvolvedor que é novo no .NET, deve estar familiarizado com a plataforma e a linguagem de sua escolha. Este artigo baseia-se estritamente nesse conhecimento e pressupõe que o programador já sabe o suficiente para executar o programa. Se você estiver portando um aplicativo existente para o .NET, vale a pena ler este documento antes de iniciar a porta. Algumas das dicas aqui são úteis na fase de design e fornecem informações que você deve estar ciente antes de começar a porta.

Este artigo é dividido em segmentos, com dicas organizadas por tipo de projeto e desenvolvedor. O primeiro conjunto de dicas é uma leitura necessária para escrever em qualquer idioma e contém conselhos que ajudarão você com qualquer linguagem de destino no CLR (Common Language Runtime). Uma seção relacionada segue as dicas específicas do ASP. O segundo conjunto de dicas é organizado por linguagem, lidando com dicas específicas sobre como usar o C++ gerenciado e o Microsoft® Visual Basic®.

Devido às limitações de agendamento, o tempo de execução da versão 1 (v1) teve que direcionar a funcionalidade mais ampla primeiro e, em seguida, lidar com otimizações de caso especial mais tarde. Isso resulta em alguns casos em que o desempenho se torna um problema. Dessa forma, este artigo aborda várias dicas projetadas para evitar esse caso. Essas dicas não serão relevantes na próxima versão (vNext), pois esses casos são sistematicamente identificados e otimizados. Eu vou apontá-los como nós vamos, e cabe a você decidir se vale a pena o esforço.

Dicas de desempenho para todos os aplicativos

Há algumas dicas para lembrar ao trabalhar no CLR em qualquer linguagem. Eles são relevantes para todos e devem ser a primeira linha de defesa ao lidar com problemas de desempenho.

Gerar menos exceções

Gerar exceções pode ser muito caro, então certifique-se de que você não jogue muitas delas. Use o Perfmon para ver quantas exceções seu aplicativo está lançando. Pode surpreendê-lo ao descobrir que determinadas áreas do seu aplicativo geram mais exceções do que o esperado. Para obter uma granularidade melhor, você também pode marcar o número de exceção programaticamente usando Contadores de Desempenho.

Localizar e criar um código com exceção pesada pode resultar em uma vitória de desempenho decente. Tenha em mente que isso não tem nada a ver com blocos try/catch: você só incorre no custo quando a exceção real é gerada. Você pode usar quantos blocos try/catch desejar. Usar exceções gratuitamente é onde você perde desempenho. Por exemplo, você deve ficar longe de coisas como usar exceções para o fluxo de controle.

Aqui está um exemplo simples de como as exceções podem ser caras: simplesmente executaremos um loop For , gerando milhares ou exceções e, em seguida, encerrando. Tente comentar a instrução throw para ver a diferença de velocidade: essas exceções resultam em uma sobrecarga tremenda.

public static void Main(string[] args){
  int j = 0;
  for(int i = 0; i < 10000; i++){
    try{   
      j = i;
      throw new System.Exception();
    } catch {}
  }
  System.Console.Write(j);
  return;   
}
  • Cuidado! O tempo de execução pode gerar exceções por conta própria! Por exemplo, Response.Redirect() gera uma exceção ThreadAbort . Mesmo que você não gere exceções explicitamente, poderá usar funções que o fazem. Certifique-se de marcar Perfmon para obter a história real e o depurador para marcar a fonte.
  • Para desenvolvedores do Visual Basic: o Visual Basic ativa a verificação de int por padrão, para garantir que coisas como exceções de estouro e divisão por zero lancem. Talvez você queira desativar isso para obter desempenho.
  • Se você usar COM, deve ter em mente que HRESULTS pode retornar como exceções. Certifique-se de manter o controle deles com cuidado.

Fazer chamadas volumosas

Uma chamada volumosa é uma chamada de função que executa várias tarefas, como um método que inicializa vários campos de um objeto . Isso deve ser exibido em chamadas tagarelas, que fazem tarefas muito simples e exigem várias chamadas para fazer as coisas (como definir cada campo de um objeto com uma chamada diferente). É importante fazer chamadas volumosas, em vez de chamadas tagarelas entre métodos em que a sobrecarga é maior do que para chamadas simples de método intra-AppDomain. P/Invoke, chamadas de interoperabilidade e comunicação remota carregam sobrecarga e você deseja usá-las com moderação. Em cada um desses casos, você deve tentar projetar seu aplicativo para que ele não dependa de chamadas pequenas e frequentes que carregam tanta sobrecarga.

Uma transição ocorre sempre que o código gerenciado é chamado do código não gerenciado e vice-versa. O tempo de execução torna extremamente fácil para o programador fazer interoperabilidade, mas isso tem um preço de desempenho. Quando ocorre uma transição, as seguintes etapas precisam ser executadas:

  • Executar marshaling de dados
  • Corrigir Convenção de Chamada
  • Proteger registros salvos pelo computador chamado
  • Alternar o modo de thread para que o GC não bloqueie threads não gerenciados
  • Criar um quadro de Tratamento de Exceções em chamadas em código gerenciado
  • Assumir o controle do thread (opcional)

Para acelerar o tempo de transição, tente usar P/Invoke quando puder. A sobrecarga é de apenas 31 instruções mais o custo de marshaling se o marshalling de dados for necessário e apenas 8 caso contrário. A interoperabilidade COM é muito mais cara, recebendo mais de 65 instruções.

O marshalling de dados nem sempre é caro. Tipos primitivos exigem quase nenhum marshalling, e classes com layout explícito também são baratas. A lentidão real ocorre durante a tradução de dados, como a conversão de texto de ASCI para Unicode. Certifique-se de que os dados passados pelo limite gerenciado só sejam convertidos se precisarem: pode ser que, simplesmente, concordando com um determinado tipo de dados ou formato em seu programa, você pode reduzir muita sobrecarga de marshaling.

Os seguintes tipos são chamados de blittable, o que significa que eles podem ser copiados diretamente pelo limite gerenciado/não gerenciado sem nenhum marshaling: sbyte, byte, short, ushort, int, uint, long, ulong, float e double. Você pode passá-los gratuitamente, bem como ValueTypes e matrizes unidimensionais que contêm tipos blittable. Os detalhes corajosos do marshalling podem ser explorados mais adiante na Biblioteca MSDN. Eu recomendo lê-lo cuidadosamente se você gastar muito do seu tempo marshalling.

Design com ValueTypes

Use structs simples quando puder e quando não fizer muita conversão boxing e unboxing. Aqui está um exemplo simples para demonstrar a diferença de velocidade:

usando o sistema;

namespace ConsoleApplication{

  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 50000000; i++)
      {foo test = new foo(3.14);}
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 50000000; i++)
      {bar test2 = new bar(3.14); }
      System.Console.WriteLine("All done");
    }
  }
}

Ao executar este exemplo, você verá que o loop de struct é pedidos de magnitude mais rapidamente. No entanto, é importante ter cuidado com o uso de ValueTypes ao tratá-los como objetos. Isso adiciona uma sobrecarga extra de conversão boxing e unboxing ao seu programa e pode acabar custando mais do que se você tivesse ficado com objetos! Para ver isso em ação, modifique o código acima para usar uma matriz de foos e barras. Você descobrirá que o desempenho é mais ou menos igual.

Compensações ValueTypes são muito menos flexíveis que Objetos e acabam prejudicando o desempenho se usados incorretamente. Você precisa ter muito cuidado com quando e como usá-los.

Tente modificar o exemplo acima e armazenar os foos e barras dentro de matrizes ou tabelas de hash. Você verá o ganho de velocidade desaparecer, apenas com uma operação de conversão boxing e unboxing.

Você pode acompanhar o quanto você faz a caixa e a caixa de seleção examinando as alocações e coleções do GC. Isso pode ser feito usando Perfmon externamente ou Contadores de Desempenho em seu código.

Confira a discussão detalhada sobre ValueTypes em Considerações de desempenho das tecnologias de Run-Time no .NET Framework.

Usar AddRange para adicionar grupos

Use AddRange para adicionar uma coleção inteira, em vez de adicionar cada item na coleção iterativamente. Quase todos os controles e coleções do Windows têm métodos Add e AddRange e cada um é otimizado para uma finalidade diferente. Adicionar é útil para adicionar um único item, enquanto AddRange tem alguma sobrecarga extra, mas ganha ao adicionar vários itens. Aqui estão apenas algumas das classes que dão suporte a Add e AddRange:

  • StringCollection, TraceCollection etc.
  • HttpWebRequest
  • UserControl
  • ColumnHeader

Cortar seu conjunto de trabalho

Minimize o número de assemblies que você usa para manter seu conjunto de trabalho pequeno. Se você carregar um assembly inteiro apenas para usar um método, você está pagando um custo tremendo por muito pouco benefício. Veja se você pode duplicar a funcionalidade desse método usando o código que você já carregou.

Manter o controle do seu conjunto de trabalho é difícil e provavelmente pode ser objeto de um artigo inteiro. Aqui estão algumas dicas para ajudá-lo a:

  • Use vadump.exe para acompanhar seu conjunto de trabalho. Isso é discutido em outro white paper que abrange várias ferramentas para o ambiente gerenciado.
  • Examine Perfmon ou Contadores de Desempenho. Eles podem fornecer comentários detalhados sobre o número de classes que você carrega ou o número de métodos que recebem JITed. Você pode obter leituras de quanto tempo você gasta no carregador ou qual porcentagem do tempo de execução é gasto paginando.

Usar loops for para iteração de cadeia de caracteres – versão 1

Em C#, o palavra-chave foreach permite percorrer itens em uma lista, cadeia de caracteres etc. e executar operações em cada item. Essa é uma ferramenta muito poderosa, pois atua como um enumerador de uso geral em muitos tipos. A compensação para essa generalização é a velocidade e, se você depender muito da iteração de cadeia de caracteres, deverá usar um loop For . Como as cadeias de caracteres são matrizes de caracteres simples, elas podem ser andadas usando muito menos sobrecarga do que outras estruturas. O JIT é inteligente o suficiente (em muitos casos) para otimizar a verificação de limites e outras coisas dentro de um loop For , mas é proibido de fazer isso em caminhadas foreach . O resultado final é que, na versão 1, um loop For em cadeias de caracteres é até cinco vezes mais rápido do que usar foreach. Isso mudará em versões futuras, mas para a versão 1 essa é uma maneira definitiva de aumentar o desempenho.

Aqui está um método de teste simples para demonstrar a diferença de velocidade. Tente executá-lo e, em seguida, remova o loop For e remova a instrução foreach . No meu computador, o loop For levou cerca de um segundo, com cerca de 3 segundos para a instrução foreach .

public static void Main(string[] args) {
  string s = "monkeys!";
  int dummy = 0;

  System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
  for(int i = 0; i < 1000000; i++)
    sb.Append(s);
  s = sb.ToString();
  //foreach (char c in s) dummy++;
  for (int i = 0; i < 1000000; i++)
    dummy++;
  return;   
  }
}

As compensaçõesforeach são muito mais legíveis e, no futuro, ela se tornará tão rápida quanto um loop For para casos especiais como cadeias de caracteres. A menos que a manipulação de cadeia de caracteres seja uma verdadeira cadeia de desempenho para você, o código ligeiramente mais confuso pode não valer a pena.

Usar StringBuilder para manipulação de cadeia de caracteres complexa

Quando uma cadeia de caracteres é modificada, o tempo de execução criará uma nova cadeia de caracteres e a retornará, deixando o original para ser coletado. Na maioria das vezes, essa é uma maneira rápida e simples de fazer isso, mas quando uma cadeia de caracteres está sendo modificada repetidamente, ela começa a ser um fardo para o desempenho: todas essas alocações eventualmente ficam caras. Aqui está um exemplo simples de um programa que acrescenta a uma cadeia de caracteres 50.000 vezes, seguido por um que usa um objeto StringBuilder para modificar a cadeia de caracteres no local. O código StringBuilder é muito mais rápido e, se você executá-los, ele se torna imediatamente óbvio.

namespace ConsoleApplication1.Feedback{
  using System;
  
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      String str = test.text;
      for(int i=0;i<50000;i++){
        str = str + "blue_toothbrush";
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}
namespace ConsoleApplication1.Feedback{
  using System;
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      System.Text.StringBuilder SB = 
        new System.Text.StringBuilder(test.text);
      for(int i=0;i<50000;i++){
        SB.Append("blue_toothbrush");
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}

Tente examinar Perfmon para ver quanto tempo é salvo sem alocar milhares de cadeias de caracteres. Examine o contador "% de tempo no GC" na lista Memória do CLR do .NET. Você também pode acompanhar o número de alocações salvas, bem como estatísticas de coleção.

Compensações= Há alguma sobrecarga associada à criação de um objeto StringBuilder , tanto no tempo quanto na memória. Em um computador com memória rápida, um StringBuilder se tornará útil se você estiver fazendo cerca de cinco operações. Como regra geral, eu diria que 10 ou mais operações de cadeia de caracteres são uma justificativa para a sobrecarga em qualquer computador, até mesmo uma mais lenta.

Pré-compilar aplicativos Windows Forms

Os métodos são JITed quando são usados pela primeira vez, o que significa que você paga uma penalidade de inicialização maior se o aplicativo faz muitas chamadas de método durante a inicialização. Windows Forms usar muitas bibliotecas compartilhadas no sistema operacional e a sobrecarga ao iniciá-las pode ser muito maior do que outros tipos de aplicativos. Embora nem sempre seja o caso, a pré-compilação Windows Forms aplicativos geralmente resulta em uma vitória de desempenho. Em outros cenários, geralmente é melhor deixar o JIT cuidar dele, mas se você for um desenvolvedor Windows Forms, convém dar uma olhada.

A Microsoft permite pré-compilar um aplicativo chamando ngen.exe. Você pode optar por executar ngen.exe durante o tempo de instalação ou antes de distribuir seu aplicativo. Definitivamente, faz mais sentido executar ngen.exe durante o tempo de instalação, pois você pode garantir que o aplicativo esteja otimizado para o computador no qual ele está sendo instalado. Se você executar ngen.exe antes de enviar o programa, limite as otimizações para as disponíveis em seu computador. Para lhe dar uma ideia de quanta pré-compilação pode ajudar, fiz um teste informal no meu computador. Abaixo estão os tempos de inicialização frios para ShowFormComplex, um aplicativo winforms com cerca de cem controles.

Estado do código Hora
JiTed de Estrutura

ShowFormComplex JITed

3,4 s
Estrutura pré-compilada, ShowFormComplex JITed 2,5 s
Estrutura pré-compilada, ShowFormComplex pré-compilado 2.1sec

Cada teste foi executado após uma reinicialização. Como você pode ver, Windows Forms aplicativos usam muitos métodos antecipadamente, tornando-se uma vitória de desempenho substancial para pré-compilar.

Usar matrizes jagged — versão 1

O JIT v1 otimiza matrizes irregulares (simplesmente 'matrizes de matrizes') com mais eficiência do que matrizes retangulares, e a diferença é bastante perceptível. Aqui está uma tabela que demonstra o ganho de desempenho resultante do uso de matrizes irregulares no lugar das retangulares no C# e no Visual Basic (números mais altos são melhores):

  C# Visual Basic 7
Atribuição (irregular)

Atribuição (retangular)

14.16

8.37

12.24

8.62

Rede Neural (irregular)

Rede neural (retangular)

4.48

3.00

4.58

3.13

Classificação Numérica (irregular)

Classificação Numérica (retangular)

4.88

2.05

5.07

2.06

O parâmetro de comparação de atribuição é um algoritmo de atribuição simples, adaptado do guia passo a passo encontrado na Tomada de Decisões Quantitativas para Empresas (Gordon, Pressman e Cohn; Prentice-Hall; fora da impressão). O teste de rede neural executa uma série de padrões em uma pequena rede neural e a classificação numérica é autoexplicativa. Juntos, esses parâmetros de comparação representam uma boa indicação do desempenho do mundo real.

Como você pode ver, o uso de matrizes irregulares pode resultar em aumentos de desempenho bastante dramáticos. As otimizações feitas em matrizes irregulares serão adicionadas a versões futuras do JIT, mas para v1 você pode economizar muito tempo usando matrizes irregulares.

Manter o tamanho do buffer de E/S entre 4KB e 8KB

Para quase todos os aplicativos, um buffer entre 4KB e 8KB lhe dará o desempenho máximo. Para instâncias muito específicas, talvez você consiga obter uma melhoria de um buffer maior (carregando imagens grandes de um tamanho previsível, por exemplo), mas em 99,99% dos casos, ele apenas desperdiçará memória. Todos os buffers derivados de BufferedStream permitem que você defina o tamanho como qualquer coisa desejada, mas, na maioria dos casos, 4 e 8 lhe darão o melhor desempenho.

Estar no Lookout para oportunidades de E/S assíncronas

Em casos raros, você pode ser capaz de se beneficiar da E/S assíncrona. Um exemplo pode ser baixar e descompactar uma série de arquivos: você pode ler os bits de um fluxo, decodificá-los na CPU e gravá-los em outro. É preciso muito esforço para usar a E/S assíncrona com eficiência, e isso pode resultar em uma perda de desempenho se não for feito corretamente. A vantagem é que, quando aplicada corretamente, a E/S assíncrona pode fornecer até dez vezes o desempenho.

Um excelente exemplo de um programa usando E/S assíncrona está disponível no Biblioteca MSDN.

  • Uma coisa a observar é que há uma pequena sobrecarga de segurança para chamadas assíncronas: ao invocar uma chamada assíncrona, o estado de segurança da pilha do chamador é capturado e transferido para o thread que realmente executará a solicitação. Isso pode não ser uma preocupação se o retorno de chamada executar muito código ou se as chamadas assíncronas não forem usadas excessivamente

Dicas para acesso ao banco de dados

A filosofia de ajuste para o acesso ao banco de dados é usar apenas a funcionalidade de que você precisa e projetar em torno de uma abordagem "desconectada": fazer várias conexões em sequência, em vez de manter uma única conexão aberta por um longo tempo. Você deve levar essa alteração em conta e projetar em torno dela.

A Microsoft recomenda uma estratégia de N Camadas para o desempenho máximo, em vez de uma conexão direta de cliente para banco de dados. Considere isso como parte de sua filosofia de design, já que muitas das tecnologias em vigor são otimizadas para aproveitar um cenário de vários cansados.

Usar a provedor gerenciado Ideal

Faça a escolha correta do provedor gerenciado, em vez de depender de um acessador genérico. Há provedores gerenciados escritos especificamente para muitos bancos de dados diferentes, como SQL (System.Data.SqlClient). Se você usar uma interface mais genérica, como System.Data.Odbc, quando puder usar um componente especializado, perderá o desempenho lidando com o nível adicional de indireção. Usar o provedor ideal também pode fazer você falar uma linguagem diferente: o Cliente SQL Gerenciado fala TDS para um banco de dados SQL, fornecendo uma melhoria dramática sobre o OleDbprotocol genérico.

Escolher leitor de dados sobre o conjunto de dados quando puder

Use um leitor de dados sempre que não precisar manter os dados por aí. Isso permite uma leitura rápida dos dados, que podem ser armazenados em cache se o usuário desejar. Um leitor é simplesmente um fluxo sem estado que permite que você leia os dados à medida que eles chegam e, em seguida, solte-os sem armazená-los em um conjunto de dados para obter mais navegação. A abordagem de fluxo é mais rápida e tem menos sobrecarga, pois você pode começar a usar dados imediatamente. Você deve avaliar a frequência com que precisa dos mesmos dados para decidir se o cache para navegação faz sentido para você. Aqui está uma pequena tabela que demonstra a diferença entre DataReader e DataSet em provedores ODBC e SQL ao extrair dados de um servidor (números mais altos são melhores):

  ADO SQL
DataSet 801 2507
DataReader 1083 4585

Como você pode ver, o desempenho mais alto é obtido ao usar o provedor gerenciado ideal junto com um leitor de dados. Quando você não precisa armazenar em cache seus dados, o uso de um leitor de dados pode fornecer um enorme aumento de desempenho.

Usar Mscorsvr.dll para computadores MP

Para aplicativos autônomos de camada intermediária e de servidor, verifique se mscorsvr está sendo usado para computadores multiprocessadores. O Mscorwks não é otimizado para dimensionamento ou taxa de transferência, enquanto a versão do servidor tem várias otimizações que permitem dimensionar bem quando mais de um processador estiver disponível.

Usar procedimentos armazenados sempre que possível

Os procedimentos armazenados são ferramentas altamente otimizadas que resultam em excelente desempenho quando usados com eficiência. Configure procedimentos armazenados para manipular inserções, atualizações e exclusões com o adaptador de dados. Os procedimentos armazenados não precisam ser interpretados, compilados ou mesmo transmitidos do cliente e reduzir o tráfego de rede e a sobrecarga do servidor. Use CommandType.StoredProcedure em vez de CommandType.Text

Tenha cuidado com cadeias de conexão dinâmicas

O pool de conexões é uma maneira útil de reutilizar conexões para várias solicitações, em vez de pagar a sobrecarga de abrir e fechar uma conexão para cada solicitação. Ela é feita implicitamente, mas você obtém um pool por cadeia de conexão exclusiva. Se você estiver gerando cadeias de conexão dinamicamente, verifique se as cadeias de caracteres são idênticas sempre para que o pool ocorra. Lembre-se também de que, se a delegação estiver ocorrendo, você receberá um pool por usuário. Há muitas opções que você pode definir para o pool de conexões e você pode acompanhar o desempenho do pool usando o Perfmon para acompanhar coisas como tempo de resposta, transações/s etc.

Desativar recursos que você não usa

Desative a inscrição automática de transações se ela não for necessária. Para o provedor gerenciado SQL, ele é feito por meio da cadeia de conexão:

SqlConnection conn = new SqlConnection(
"Server=mysrv01;
Integrated Security=true;
Enlist=false");

Ao preencher um conjunto de dados com o adaptador de dados, não obtenha informações de chave primária se você não precisar (por exemplo, não definir MissingSchemaAction.Add com chave):

public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
    SqlConnection conn = new SqlConnection(connection);
    SqlDataAdapter adapter = new SqlDataAdapter();
    adapter.SelectCommand = new SqlCommand(query, conn);
    adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
    adapter.Fill(dataset);
    return dataset;
}

Evitar comandos gerados automaticamente

Ao usar um adaptador de dados, evite comandos gerados automaticamente. Elas exigem viagens adicionais ao servidor para recuperar metadados e fornecem um nível mais baixo de controle de interação. Embora o uso de comandos gerados automaticamente seja conveniente, vale a pena o esforço para fazer isso por conta própria em aplicativos críticos de desempenho.

Cuidado com o design herdado do ADO

Lembre-se de que, quando você executa um comando ou preenchimento de chamada no adaptador, todos os registros especificados pela consulta são retornados.

Se os cursores de servidor forem absolutamente necessários, eles poderão ser implementados por meio de um procedimento armazenado no t-sql. Evite sempre que possível porque as implementações baseadas em cursor de servidor não são muito bem dimensionadas.

Se necessário, implemente a paginação de maneira sem estado e sem conexão. Você pode adicionar registros adicionais ao conjunto de dados:

  • Verificar se as informações do PK estão presentes
  • Alterando o comando select do adaptador de dados conforme apropriado e
  • Chamando Preenchimento

Manter seus conjuntos de dados lean

Coloque apenas os registros necessários no conjunto de dados. Lembre-se de que o conjunto de dados armazena todos os seus dados na memória e que quanto mais dados você solicitar, mais tempo levará para transmitir pela transmissão.

Usar o acesso sequencial com a maior frequência possível

Com um leitor de dados, use CommandBehavior.SequentialAccess. Isso é essencial para lidar com tipos de dados de blob, pois permite que os dados sejam lidos fora do fio em pequenos pedaços. Embora você só possa trabalhar com uma parte dos dados por vez, a latência para carregar um tipo de dados grande desaparece. Se você não precisar trabalhar o objeto inteiro de uma só vez, o uso do Acesso Sequencial lhe dará um desempenho muito melhor.

Dicas de desempenho para aplicativos ASP.NET

Armazenar em cache agressivamente

Ao criar um aplicativo usando ASP.NET, certifique-se de projetar de olho no cache. Nas versões do servidor do sistema operacional, você tem muitas opções para ajustar o uso de caches no servidor e no lado do cliente. Há vários recursos e ferramentas no ASP que você pode usar para obter desempenho.

Cache de saída — armazena o resultado estático de uma solicitação ASP. Especificado usando a <@% OutputCache %> diretiva :

  • Duração — o item de tempo existe no cache
  • VaryByParam — Varia as entradas de cache por params Get/Post
  • VaryByHeader — Varia as entradas de cache por cabeçalho Http
  • VaryByCustom — Varia as entradas de cache por navegador
  • Substitua para variar de acordo com o que você quiser:
    • Cache de fragmentos – quando não for possível armazenar uma página inteira (privacidade, personalização, conteúdo dinâmico), você poderá usar o cache de fragmentos para armazenar partes dele para recuperação mais rápida posteriormente.

      a) VaryByControl — Varia os itens armazenados em cache por valores de um controle

    • API de cache — fornece granularidade extremamente fina para o cache mantendo um hashtable de objetos armazenados em cache na memória (System.web.UI.caching). Ele também:

      a) Inclui Dependências (chave, arquivo, hora)

      b) Expira automaticamente itens não utilizados

      c) Dá suporte a retornos de chamada

O cache inteligente pode lhe dar um excelente desempenho e é importante pensar em que tipo de cache você precisa. Imagine um site de comércio eletrônico complexo com várias páginas estáticas para logon e, em seguida, uma série de páginas geradas dinamicamente contendo imagens e texto. Talvez você queira usar o Cache de Saída para essas páginas de logon e, em seguida, fragmentar o cache para as páginas dinâmicas. Uma barra de ferramentas, por exemplo, pode ser armazenada em cache como um fragmento. Para obter um desempenho ainda melhor, você pode armazenar em cache imagens usadas com frequência e texto clichê que aparecem com frequência no site usando a API de Cache. Para obter informações detalhadas sobre o cache (com código de exemplo), marcar o site do ASP. NET.

Usar o estado da sessão somente se precisar

Um recurso extremamente poderoso do ASP.NET é sua capacidade de armazenar o estado da sessão para os usuários, como um carrinho de compras em um site de comércio eletrônico ou um histórico do navegador. Como isso está ativado por padrão, você paga o custo na memória mesmo que não o use. Se você não estiver usando o Estado da Sessão, desative-o e salve-se a sobrecarga adicionando <@% EnabledSessionState = false %> ao seu asp. Isso vem com várias outras opções, que são explicadas no site do ASP. NET .

Para páginas que leem apenas o estado da sessão, você pode escolher EnabledSessionState=readonly. Isso carrega menos sobrecarga do que o estado completo da sessão de leitura/gravação e é útil quando você precisa apenas de parte da funcionalidade e não quer pagar pelos recursos de gravação.

Usar o estado de exibição somente se precisar

Um exemplo de Estado de Exibição pode ser um formulário longo que os usuários devem preencher: se clicarem em Voltar no navegador e retornarem, o formulário permanecerá preenchido. Quando essa funcionalidade não é usada, esse estado consome memória e desempenho. Talvez o maior esvaziamento de desempenho aqui seja que um sinal de ida e volta deve ser enviado pela rede sempre que a página é carregada para atualizar e verificar o cache. Como ele está ativado por padrão, você precisará especificar que não deseja usar o Estado de Exibição com <@% EnabledViewState = false %>. Você deve ler mais sobre Exibir Estado no site do ASP. NET para saber mais sobre algumas das outras opções e configurações às quais você tem acesso.

Evitar STA COM

O Apartment COM foi projetado para lidar com threading em ambientes não gerenciados. Há dois tipos de Apartment COM: single-threaded e multithreaded. O MTA COM foi projetado para lidar com multithreading, enquanto o STA COM depende do sistema de mensagens para serializar solicitações de thread. O mundo gerenciado é de thread livre e o uso do Single Threaded Apartment COM requer que todos os threads não gerenciados compartilhem essencialmente um único thread para interoperabilidade. Isso resulta em um grande impacto no desempenho e deve ser evitado sempre que possível. Se você não conseguir portar o objeto APARTMENT COM para o mundo gerenciado, use <@%AspCompat = %> "true" para páginas que os usam. Para obter uma explicação mais detalhada do STA COM, consulte o Biblioteca MSDN.

Compilar em Lote

Sempre compile em lote antes de implantar uma página grande na Web. Isso pode ser iniciado fazendo uma solicitação para uma página por diretório e aguardando até que a CPU ociosa novamente. Isso impede que o servidor Web seja atolado com compilações enquanto também tenta fornecer páginas.

Remover módulos Http desnecessários

Dependendo dos recursos usados, remova módulos http não utilizados ou desnecessários do pipeline. Recuperar a memória adicionada e os ciclos desperdiçados pode fornecer um pequeno aumento de velocidade.

Evitar o recurso Autoeventwireup

Em vez de depender do autoeventwireup, substitua os eventos de Page. Por exemplo, em vez de escrever um método Page_Load(), tente sobrecarregar o método OnLoad() nulo público. Isso permite que o tempo de execução tenha que fazer um CreateDelegate() para cada página.

Codificar usando ASCII quando você não precisa de UTF

Por padrão, ASP.NET vem configurado para codificar solicitações e respostas como UTF-8. Se ASCII for tudo o que seu aplicativo precisa, a sobrecarga UTF eliminada poderá retornar alguns ciclos. Observe que isso só pode ser feito por aplicativo.

Usar o procedimento de autenticação ideal

Há várias maneiras diferentes de autenticar um usuário e algumas mais caras do que outras (em ordem de aumento de custo: Nenhum, Windows, Forms, Passport). Certifique-se de usar o mais barato que melhor atenda às suas necessidades.

Dicas para portabilidade e desenvolvimento no Visual Basic

Muita coisa mudou nos bastidores do Microsoft® Visual Basic® 6 para o Microsoft® Visual Basic® 7 e o mapa de desempenho mudou com ele. Devido à funcionalidade adicionada e às restrições de segurança do CLR, algumas funções simplesmente não podem ser executadas tão rapidamente quanto no Visual Basic 6. Na verdade, há várias áreas em que o Visual Basic 7 é substituído por seu antecessor. Felizmente, há duas boas notícias:

  • A maioria das piores lentidão ocorre durante funções de uso único, como carregar um controle pela primeira vez. O custo está lá, mas você só paga uma vez.
  • Há muitas áreas em que o Visual Basic 7 é mais rápido e essas áreas tendem a estar em funções repetidas durante o tempo de execução. Isso significa que o benefício cresce ao longo do tempo e, em vários casos, superará os custos únicos.

A maioria dos problemas de desempenho vem de áreas em que o tempo de execução não dá suporte a um recurso do Visual Basic 6 e precisa ser adicionado para preservar o recurso no Visual Basic 7. Trabalhar fora do tempo de execução é mais lento, tornando alguns recursos muito mais caros de usar. O lado bom é que você pode evitar esses problemas com um pouco de esforço. Há duas áreas main que exigem trabalho para otimizar o desempenho e alguns ajustes simples que você pode fazer aqui e ali. Juntos, eles podem ajudá-lo a contornar os drenos de desempenho e aproveitar as funções que são muito mais rápidas no Visual Basic 7.

Tratamento de erros

A primeira preocupação é o tratamento de erros. Isso mudou muito no Visual Basic 7 e há problemas de desempenho relacionados à alteração. Essencialmente, a lógica necessária para implementar OnErrorGoto e Resume é extremamente cara. Sugiro dar uma olhada rápida no código e realçar todas as áreas em que você usa o objeto Err ou qualquer mecanismo de tratamento de erros. Agora, examine cada uma dessas instâncias e veja se você pode reescrevê-las para usar try/catch. Muitos desenvolvedores descobrirão que podem converter para experimentar/capturar facilmente para a maioria desses casos, e eles devem ver uma boa melhoria de desempenho em seu programa. A regra geral é "se você pode ver facilmente a tradução, faça isso".

Aqui está um exemplo de um programa simples do Visual Basic que usa On Error Goto em comparação com a versão try/catch .

Sub SubWithError()
On Error Goto SWETrap
  Dim x As Integer
  Dim y As Integer
  x = x / y
SWETrap:  Exit Sub
  End Sub
 
Sub SubWithErrorResumeLabel()
  On Error Goto SWERLTrap
  Dim x As Integer
  Dim y As Integer
  x = x / y 
SWERLTrap:
  Resume SWERLExit
  End Sub
SWERLExit:
  Exit Sub
Sub SubWithError()
  Dim x As Integer
  Dim y As Integer
  Try    x = x / y  Catch    Return  End Try
  End Sub
 
Sub SubWithErrorResumeLabel()
  Dim x As Integer
  Dim y As Integer
  Try
    x = x / y
  Catch
  Goto SWERLExit
  End Try
 
SWERLExit:
  Return
  End Sub

O aumento de velocidade é perceptível. SubWithError() usa 244 milissegundos usando OnErrorGoto e apenas 169 milissegundos usando try/catch. A segunda função usa 179 milissegundos em comparação com 164 milissegundos para a versão otimizada.

Usar associação antecipada

A segunda preocupação lida com objetos e typecasting. O Visual Basic 6 faz muito trabalho nos bastidores para dar suporte à conversão de objetos e muitos programadores nem sequer estão cientes disso. No Visual Basic 7, essa é uma área da qual você pode espremer muito desempenho. Ao compilar, use a associação antecipada. Isso instrui o compilador a inserir uma Coerção de Tipo só é feita quando mencionado explicitamente. Isso tem dois efeitos principais:

  • Erros estranhos tornam-se mais fáceis de rastrear.
  • Coerções desnecessárias são eliminadas, levando a melhorias substanciais no desempenho.

Quando você usa um objeto como se ele fosse de um tipo diferente, o Visual Basic coagirá o objeto para você se você não especificar. Isso é útil, pois o programador precisa se preocupar com menos código. A desvantagem é que essas coerções podem fazer coisas inesperadas, e o programador não tem controle sobre elas.

Há instâncias em que você precisa usar a associação tardia, mas na maioria das vezes, se não tiver certeza, poderá se safar com a associação antecipada. Para programadores do Visual Basic 6, isso pode ser um pouco estranho no início, pois você precisa se preocupar com tipos mais do que no passado. Isso deve ser fácil para novos programadores, e as pessoas familiarizadas com o Visual Basic 6 o pegarão em pouco tempo.

Ativar opção estrita e explícita

Com Option Strict ativado, você se protege contra associação tardia inadvertida e impõe um nível mais alto de disciplina de codificação. Para obter uma lista das restrições presentes com Option Strict, consulte o Biblioteca MSDN. A ressalva a isso é que todas as coerções de tipo de restrição devem ser especificadas explicitamente. No entanto, isso por si só pode descobrir outras seções do seu código que estão fazendo mais trabalho do que você pensava anteriormente, e isso pode ajudá-lo a pisar alguns bugs no processo.

Option Explicit é menos restritivo que Option Strict, mas ainda força os programadores a fornecer mais informações em seu código. Especificamente, você deve declarar uma variável antes de usá-la. Isso move a inferência de tipo do tempo de execução para o tempo de compilação. Esse marcar eliminado se traduz em desempenho adicional para você.

Recomendo que você comece com Option Explicit e ative Option Strict. Isso protegerá você de um dilúvio de erros do compilador e permitirá que você comece gradualmente a trabalhar no ambiente mais estrito. Quando ambas as opções são usadas, você garante o desempenho máximo para seu aplicativo.

Usar Comparação Binária para Texto

Ao comparar texto, use a comparação binária em vez da comparação de texto. Em tempo de execução, a sobrecarga é muito mais leve para binário.

Minimizar o uso de format()

Quando puder, use toString() em vez de format(). Na maioria dos casos, ele fornecerá a funcionalidade necessária, com muito menos sobrecarga.

Usar Charw

Use charw em vez de char. O CLR usa Unicode internamente e char deve ser convertido em tempo de execução se for usado. Isso pode resultar em uma perda substancial de desempenho e especificar que seus caracteres são uma palavra inteira longa (usar charw) elimina essa conversão.

Otimizar atribuições

Use exp += val em vez de exp = exp + val. Como exp pode ser arbitrariamente complexo, isso pode resultar em muito trabalho desnecessário. Isso força o JIT a avaliar ambas as cópias de exp e muitas vezes isso não é necessário. A primeira instrução pode ser otimizada muito melhor do que a segunda, pois o JIT pode evitar avaliar o exp duas vezes.

Evitar indireção desnecessária

Ao usar byRef, você passa ponteiros em vez do objeto real. Muitas vezes isso faz sentido (funções de efeito colateral, por exemplo), mas nem sempre você precisa dela. Passar ponteiros resulta em mais indireção, o que é mais lento do que acessar um valor que está na pilha. Quando você não precisa passar pelo heap, é melhor evitá-lo.

Colocar concatenações em uma expressão

Se você tiver várias concatenações em várias linhas, tente colocá-las todas em uma expressão. O compilador pode otimizar modificando a cadeia de caracteres em vigor, fornecendo um aumento de velocidade e memória. Se as instruções forem divididas em várias linhas, o compilador do Visual Basic não gerará a MSIL (Linguagem Intermediária da Microsoft) para permitir a concatenação in-loco. Consulte o exemplo StringBuilder discutido anteriormente.

Incluir instruções return

O Visual Basic permite que uma função retorne um valor sem usar a instrução return . Embora o Visual Basic 7 dê suporte a isso, usar explicitamente o retorno permite que o JIT execute um pouco mais de otimizações. Sem uma instrução return, cada função recebe várias variáveis locais na pilha para dar suporte transparente a valores retornados sem o palavra-chave. Mantê-los ao redor torna mais difícil para o JIT otimizar e pode afetar o desempenho do seu código. Examine suas funções e insira o retorno conforme necessário. Ele não altera a semântica do código e pode ajudá-lo a obter mais velocidade do seu aplicativo.

Dicas para portabilidade e desenvolvimento no C++ gerenciado

A Microsoft está direcionando o MC++ (Managed C++) para um conjunto específico de desenvolvedores. O MC++ não é a melhor ferramenta para cada trabalho. Depois de ler este documento, você pode decidir que o C++ não é a melhor ferramenta e que os custos de compensação não valem os benefícios. Se você não tiver certeza sobre o MC++, há muitos recursos bons para ajudá-lo a tomar sua decisão Esta seção é direcionada a desenvolvedores que já decidiram que desejam usar o MC++ de alguma forma e querem saber sobre os aspectos de desempenho dele.

Para desenvolvedores do C++, trabalhar em C++ Gerenciado exige que várias decisões sejam tomadas. Você está portando um código antigo? Nesse caso, você deseja mover tudo para o espaço gerenciado ou planeja implementar um wrapper? Vou me concentrar na opção "port-everything" ou lidar com a escrita do MC++ do zero para fins desta discussão, já que esses são os cenários em que o programador observará uma diferença de desempenho.

Benefícios do Mundo Gerenciado

O recurso mais poderoso do C++ Gerenciado é a capacidade de misturar e corresponder código gerenciado e não gerenciado no nível da expressão. Nenhum outro idioma permite que você faça isso, e há alguns benefícios poderosos que você pode obter com ele se usado corretamente. Vou examinar alguns exemplos disso mais tarde.

O mundo gerenciado também lhe dá enormes vitórias de design, pois muitos problemas comuns são resolvidos para você. O gerenciamento de memória, o agendamento de threads e as coerções de tipo podem ser deixados para o tempo de execução, se desejar, permitindo que você concentre suas energias nas partes do programa que precisam dele. Com o MC++, você pode escolher exatamente quanto controle deseja manter.

Os programadores mc++ têm o luxo de poder usar o back-end do Microsoft Visual C® 7 (VC7) ao compilar para IL e, em seguida, usar o JIT em cima disso. Os programadores que estão acostumados a trabalhar com o compilador do Microsoft C++ estão acostumados com coisas que são rápidas. O JIT foi projetado com objetivos diferentes, e tem um conjunto diferente de pontos fortes e fracos. O compilador VC7, não associado pelas restrições de tempo do JIT, pode executar determinadas otimizações que o JIT não pode, como análise de programas inteiros, inlining e registro mais agressivos. Também há algumas otimizações que podem ser executadas apenas em ambientes typesafe, deixando mais espaço para velocidade do que o C++ permite.

Devido às diferentes prioridades no JIT, algumas operações são mais rápidas do que antes, enquanto outras são mais lentas. Há compensações que você faz para a flexibilidade de segurança e linguagem, e algumas delas não são baratas. Felizmente, há coisas que um programador pode fazer para minimizar os custos.

Portabilidade: todo código C++ pode ser compilado para MSIL

Antes de prosseguirmos, é importante observar que você pode compilar qualquer código C++ no MSIL. Tudo funcionará, mas não há garantia de segurança de tipo e você paga a penalidade de marshalling se você fizer muita interoperabilidade. Por que é útil compilar para o MSIL se você não receber nenhum dos benefícios? Em situações em que você está portando uma base de código grande, isso permite portar gradualmente seu código em partes. Você pode gastar seu tempo portando mais código, em vez de escrever wrappers especiais para colar o código portado e ainda não portado se você usar MC++, e isso pode resultar em uma grande vitória. Isso torna a portabilidade de aplicativos um processo muito limpo. Para saber mais sobre como compilar C++ para MSIL, dê uma olhada na opção do compilador /clr.

No entanto, simplesmente compilar seu código C++ para MSIL não oferece a segurança ou a flexibilidade do mundo gerenciado. Você precisa escrever no MC++e, na v1, isso significa abrir mão de alguns recursos. A lista abaixo não tem suporte na versão atual do CLR, mas pode estar no futuro. A Microsoft optou por dar suporte aos recursos mais comuns primeiro e teve que cortar alguns outros para enviar. Não há nada que os impeça de serem adicionados mais tarde, mas enquanto isso, você precisará fazer sem eles:

  • Várias heranças
  • Modelos
  • Finalização determinística

Você sempre pode interoperar com código não seguro se precisar desses recursos, mas pagará a penalidade de desempenho de realizar marshaling de dados para frente e para trás. E tenha em mente que esses recursos só podem ser usados dentro do código não gerenciado. O espaço gerenciado não tem conhecimento de sua existência. Se você estiver decidindo portar seu código, pense no quanto você depende desses recursos em seu design. Em alguns casos, a reformulação é muito cara e você vai querer manter o código não gerenciado. Esta é a primeira decisão que você deve tomar, antes de começar a hackear.

Vantagens do MC++ sobre C# ou Visual Basic

Vindo de uma tela de fundo não gerenciada, o MC++ preserva grande parte da capacidade de lidar com código não seguro. A capacidade do MC++de misturar código gerenciado e não gerenciado sem gerenciamento oferece muito poder ao desenvolvedor e você pode escolher onde no gradiente deseja sentar ao escrever seu código. Em um extremo, você pode escrever tudo em C++ reto e não adulterado e apenas compilar com /clr. Por outro lado, você pode escrever tudo como objetos gerenciados e lidar com as limitações de linguagem e problemas de desempenho mencionados acima.

Mas o verdadeiro poder do MC++ vem quando você escolhe um lugar no meio. O MC++ permite ajustar alguns dos acertos de desempenho inerentes ao código gerenciado, fornecendo a você controle preciso sobre quando usar recursos não seguros. O C# tem algumas dessas funcionalidades no palavra-chave não seguro, mas não é parte integrante da linguagem e é muito menos útil do que o MC++. Vamos analisar alguns exemplos mostrando a granularidade mais fina disponível no MC++, e falaremos sobre as situações em que ela é útil.

Ponteiros "byref" generalizados

No C#, você só pode pegar o endereço de algum membro de uma classe passando-o para um parâmetro ref . No MC++, um ponteiro byref é um constructo de primeira classe. Você pode pegar o endereço de um item no meio de uma matriz e retornar esse endereço de uma função:

Byte* AddrInArray( Byte b[] ) {
   return &b[5];
}

Exploramos esse recurso para retornar um ponteiro para os "caracteres" em um System.String por meio de nossa rotina auxiliar e podemos até mesmo percorrer matrizes usando estes ponteiros:

System::Char* PtrToStringChars(System::String*);   
for( Char*pC = PtrToStringChars(S"boo");
  pC != NULL;
  pC++ )
{
      ... *pC ...
}

Você também pode fazer uma passagem de lista vinculada com injeção no MC++ tomando o endereço do campo "próximo" (o que você não pode fazer em C#):

Node **w = &Head;
while(true) {
  if( *w == 0 || val < (*w)->val ) {
    Node *t = new Node(val,*w);
    *w = t;
    break;
  }
  w = &(*w)->next;
}

Em C#, você não pode apontar para "Cabeçalho" ou pegar o endereço do campo "próximo", portanto, você fez um caso especial em que está inserindo no primeiro local ou se "Cabeça" é nula. Além disso, você precisa procurar um nó à frente o tempo todo no código. Compare isso com o que um bom C# produziria:

if( Head==null || val < Head.val ) {
  Node t = new Node(val,Head);
  Head = t;
}else{
  // we know at least one node exists,
  // so we can look 1 node ahead
  Node w=Head;
while(true) {
  if( w.next == null || val < w.next.val ){
    Node t = new Node(val,w.next.next);
    w.next = t;
    break;
  }
  w = w.next;
  }
}         

Acesso do usuário a tipos boxed

Um problema de desempenho comum com idiomas de OO é o tempo gasto em conversão boxing e valores de unboxing. O MC++ oferece muito mais controle sobre esse comportamento, portanto, você não precisará fazer a unbox dinamicamente (ou estaticamente) para acessar valores. Esse é outro aprimoramento de desempenho. Basta colocar __box palavra-chave antes de qualquer tipo para representar seu formulário em caixa:

__value struct V {
  int i;
};
int main() {
  V v = {10};
  __box V *pbV = __box(v);
  pbV->i += 10;           // update without casting
}

Em C#, você precisa desem caixa para um "v" e, em seguida, atualizar o valor e reemplosar novamente para um Objeto:

struct B { public int i; }
static void Main() {
  B b = new B();
  b.i = 5;
  object o = b;         // implicit box
  B b2 = (B)o;            // explicit unbox
  b2.i++;               // update
  o = b2;               // implicit re-box
}

Coleções STL versus Coleções Gerenciadas — v1

A má notícia: no C++, o uso das Coleções STL geralmente era tão rápido quanto escrever essa funcionalidade manualmente. As estruturas CLR são muito rápidas, mas sofrem de problemas de conversão boxing e unboxing: tudo é um objeto e, sem suporte de modelo ou genérico, todas as ações precisam ser verificadas em tempo de execução.

A boa notícia: a longo prazo, você pode apostar que esse problema desaparecerá à medida que os genéricos forem adicionados ao tempo de execução. O código implantado hoje experimentará o aumento de velocidade sem nenhuma alteração. No curto prazo, você pode usar a conversão estática para evitar o marcar, mas isso não é mais seguro. É recomendável usar esse método em um código rígido em que o desempenho é absolutamente crítico e você identificou dois ou três pontos de acesso.

Usar objetos gerenciados do Stack

No C++, você especifica que um objeto deve ser gerenciado pela pilha ou pelo heap. Você ainda pode fazer isso no MC++, mas há restrições que você deve estar ciente. O CLR usa ValueTypes para todos os objetos gerenciados por pilha e há limitações para o que ValueTypes pode fazer (sem herança, por exemplo). Mais informações estão disponíveis no Biblioteca MSDN.

Maiúsculas e minúsculas: cuidado com chamadas indiretas dentro do código gerenciado — v1

No tempo de execução v1, todas as chamadas de função indiretas são feitas nativamente e, portanto, exigem uma transição para o espaço não gerenciado. Qualquer chamada de função indireta só pode ser feita no modo nativo, o que significa que todas as chamadas indiretas do código gerenciado precisam de uma transição gerenciada para não gerenciada. Esse é um problema sério quando a tabela retorna uma função gerenciada, pois uma segunda transição deve ser feita para executar a função. Quando comparado com o custo da execução de uma única instrução call , o custo é 50 a 100 vezes mais lento do que em C++!

Felizmente, quando você está chamando um método que reside em uma classe coletada por lixo, a otimização remove isso. No entanto, no caso específico de um arquivo C++ regular que foi compilado usando /clr, o retorno do método será considerado gerenciado. Como isso não pode ser removido pela otimização, você é atingido com o custo total de transição dupla. Veja abaixo um exemplo desse caso.

//////////////////////// a.h:    //////////////////////////
class X {
public:
   void mf1();
   void mf2();
};

typedef void (X::*pMFunc_t)();


////////////// a.cpp: compiled with /clr  /////////////////
#include "a.h"

int main(){
   pMFunc_t pmf1 = &X::mf1;
   pMFunc_t pmf2 = &X::mf2;

   X *pX = new X();
   (pX->*pmf1)();
   (pX->*pmf2)();

   return 0;
}


////////////// b.cpp: compiled without /clr /////////////////
#include "a.h"

void X::mf1(){}


////////////// c.cpp: compiled with /clr ////////////////////
#include "a.h"
void X::mf2(){}

Há várias maneiras de evitar isso:

  • Transformar a classe em uma classe gerenciada ("__gc")
  • Remover a chamada indireta, se possível
  • Deixe a classe compilada como código não gerenciado (por exemplo, não use /clr)

Minimizar ocorrências de desempenho — versão 1

Há várias operações ou recursos que são simplesmente mais caros no MC++ na versão 1 JIT. Eu vou listá-los e dar alguma explicação, e então nós vamos falar sobre o que você pode fazer sobre eles.

  • Abstrações — Essa é uma área em que o compilador de back-end C++ lento e forte ganha muito sobre o JIT. Se você encapsular um int dentro de uma classe para fins de abstração e acessá-lo estritamente como um int, o compilador C++ poderá reduzir a sobrecarga do wrapper para praticamente nada. Você pode adicionar muitos níveis de abstração ao wrapper, sem aumentar o custo. O JIT não pode levar o tempo necessário para eliminar esse custo, tornando as abstrações profundas mais caras no MC++.
  • Ponto Flutuante – o JIT v1 atualmente não executa todas as otimizações específicas de FP que o back-end vc++ faz, tornando as operações de ponto flutuante mais caras por enquanto.
  • Matrizes Multidimensionais — o JIT é melhor no tratamento de matrizes irregulares do que as multidimensionais, portanto, use matrizes irregulares.
  • Aritmética de 64 bits— Em versões futuras, otimizações de 64 bits serão adicionadas ao JIT.

O que você pode fazer

Em cada fase de desenvolvimento, há várias coisas que você pode fazer. Com o MC++, a fase de design talvez seja a área mais importante, pois determinará quanto trabalho você acaba fazendo e quanto desempenho você obtém em troca. Ao sentar-se para gravar ou portar um aplicativo, você deve considerar as seguintes coisas:

  • Identifique as áreas em que você usa várias heranças, modelos ou finalização determinística. Você terá que se livrar deles ou então deixar essa parte do código no espaço não gerenciado. Pense no custo da reprojetação e identifique as áreas que podem ser portadas.
  • Localize pontos de acesso de desempenho, como abstrações profundas ou chamadas de função virtual em todo o espaço gerenciado. Isso também exigirá uma decisão de design.
  • Procure objetos que foram especificados como gerenciados por pilha. Verifique se eles podem ser convertidos em ValueTypes. Marque os outros para conversão em objetos gerenciados por heap.

Durante a fase de codificação, você deve estar ciente das operações mais caras e das opções que você tem para lidar com elas. Uma das coisas mais interessantes sobre o MC++ é que você começa a lidar com todos os problemas de desempenho antecipadamente, antes de começar a codificar: isso é útil para analisar o trabalho mais tarde. No entanto, ainda há alguns ajustes que você pode executar enquanto codifica e depura.

Determine quais áreas fazem uso intenso de matrizes aritméticas de ponto flutuante, multidimensionais ou funções de biblioteca. Quais dessas áreas são críticas ao desempenho? Use os criadores de perfil para escolher os fragmentos em que a sobrecarga está lhe custando mais e escolha qual opção parece melhor:

  • Mantenha todo o fragmento no espaço não gerenciado.
  • Use conversões estáticas nos acessos da biblioteca.
  • Tente ajustar o comportamento de boxing/unboxing (explicado posteriormente).
  • Codifique sua própria estrutura.

Por fim, trabalhe para minimizar o número de transições que você faz. Se você tiver algum código não gerenciado ou uma chamada de interoperabilidade em um loop, torne o loop inteiro não gerenciado. Dessa forma, você só pagará o custo de transição duas vezes, em vez de para cada iteração do loop.

Recursos adicionais

Os tópicos relacionados sobre o desempenho no .NET Framework incluem:

Assista a artigos futuros atualmente em desenvolvimento, incluindo uma visão geral das filosofias de design, arquitetura e codificação, um passo a passo das ferramentas de análise de desempenho no mundo gerenciado e uma comparação de desempenho do .NET com outros aplicativos empresariais disponíveis hoje.

Apêndice: custo de chamadas virtuais e alocações

Tipo de chamada # Chamadas/s
Chamada Não Virtual ValueType 809971805.600
Chamada não virtual de classe 268478412.546
Chamada Virtual de Classe 109117738.369
Chamada de ValueType Virtual (Método Obj) 3004286.205
Chamada de ValueType Virtual (método Obj substituído) 2917140.844
Tipo de carga por novo (não estático) 1434.720
Tipo de carga por newing (métodos virtuais) 1369.863

Observação O computador de teste é um PIII 733Mhz, executando o Windows 2000 Professional com Service Pack 2.

Esse gráfico compara o custo associado a diferentes tipos de chamadas de método, bem como o custo de instanciar um tipo que contém métodos virtuais. Quanto maior o número, mais chamadas/instanciações por segundo podem ser executadas. Embora esses números certamente variem em diferentes computadores e configurações, o custo relativo de executar uma chamada sobre outra permanece significativo.

  • Chamada Não Virtual ValueType: esse teste chama um método não virtual vazio contido em um ValueType.
  • Chamada não virtual de classe: esse teste chama um método não virtual vazio contido em uma classe.
  • Chamada Virtual de Classe: esse teste chama um método virtual vazio contido em uma classe.
  • Chamada de ValueType Virtual (Método Obj): esse teste chama ToString() (um método virtual) em um ValueType, que recorre ao método de objeto padrão.
  • Chamada de ValueType Virtual (Método Obj Substituído): esse teste chama ToString() (um método virtual) em um ValueType que substituiu o padrão.
  • Tipo de carga por Novo (Estático): esse teste aloca espaço para uma classe com apenas métodos estáticos.
  • Tipo de Carga por Novo (Métodos Virtuais): esse teste aloca espaço para uma classe com métodos virtuais.

Uma conclusão que você pode tirar é que as chamadas de Função Virtual são cerca de duas vezes mais caras do que as chamadas regulares quando você está chamando um método em uma classe. Tenha em mente que as chamadas são baratas para começar, então eu não removeria todas as chamadas virtuais. Você sempre deve usar métodos virtuais quando faz sentido fazê-lo.

  • O JIT não pode ser embutido em métodos virtuais, portanto, você perderá uma otimização potencial se se livrar de métodos não virtuais.
  • Alocar espaço para um objeto que tem métodos virtuais é um pouco mais lento do que a alocação de um objeto sem eles, uma vez que trabalho extra deve ser feito para encontrar espaço para as tabelas virtuais.

Observe que chamar um método não virtual em um ValueType é mais de três vezes mais rápido que em uma classe, mas uma vez que você o trata como uma classe , você perde terrivelmente. Isso é característica de ValueTypes: trate-os como structs e eles estão acendendo rapidamente. Trate-os como aulas e eles são dolorosamente lentos. ToString() é um método virtual, portanto, antes de ser chamado, o struct deve ser convertido em um objeto no heap. Em vez de ser duas vezes mais lento, chamar um método virtual em um ValueType agora é dezoito vezes mais lento! A moral da história? Não trate ValueTypes como classes.

Se você tiver dúvidas ou comentários sobre este artigo, entre em contato com Claudio Caldato, gerente de programas para .NET Framework problemas de desempenho.