Este artigo foi traduzido por máquina.

Tudo sobre o CLR

Uso de memória de auditoria para aplicativos .NET

Subramanian Ramaswamy and Vance Morrison

Conteúdo

Quando o uso de memória afeta velocidade
O que pode ser concluído?
O Gerenciador de tarefas
Compartilhado versus memória não compartilhada
Tamanho do aplicativo
VADump: uma exibição mais detalhada
O coletor de lixo do .NET
PerfMon
Conclusão

Otimização de desempenho é sobre uma coisa: fazer programas de computador sejam executados com mais rapidez. A execução de instruções é barata para hardware de computador modernos enquanto a busca dos operandos de instrução é cara. Assim, memória uso pode ter um impacto direto no rápido como um aplicativo é executado e é uma métrica importante para otimizar. Neste artigo, discutimos os fundamentos de otimização de memória para programas do .NET. Primeiro, descrevem os casos em que acesso à memória é um gargalo e é útil para otimizar. Em seguida, discutimos a divisão geral de como a memória é usada em um programa .NET típico. Finalmente, discutiremos ferramentas e estratégias para determinar o consumo de memória do seu aplicativo .NET e reduzir a ele.

Quando o uso de memória afeta velocidade

Primeiro caso de quando importa o consumo de memória é um aplicativo intenso da CPU que está manipulando uma grande quantidade de dados. Um PC típico pode executar uma instrução em nanosegundos menor do que metade (0,5 ns). No entanto, essa velocidade é limitada pelos quanto tempo demora para buscar os operandos de memória. Processadores modernos tem uma hierarquia de caches para otimizar o custo do hardware. O cache de nível 1 (L1) é a memória mais rápida, mas é relativamente pequeno. Em seguida na hierarquia é o cache de nível 2, seguido a memória principal (RAM) e finalmente a unidade de disco rígido. a Figura 1 mostra a hora de acesso e tamanhos de várias partes da hierarquia de memória para um PC típico. Em cada etapa mais para a hierarquia de memória, o tempo de acesso (e tamanho) aumenta em uma ordem de magnitude ou mais (unidades de disco rígido são um 10.000 vezes mais lento do que a RAM) enquanto diminui o custo (por byte).

Figura 1 tamanho e horas de acesso com o armazenamento não-local
  Cache de L1 Cache L2 Memória (RAM) Disco
Tamanho 64 K 512 K 2000M 100,000M
Tempo de acesso 0,4 ns 4 ns 40 ns 4,000,000 ns

Se caminhos de dados ativo acessam mais memória, em seguida, os operandos freqüentemente precisará ser obtidas de memória mais lenta. Como a memória mais lenta é mais lenta por uma ordem de magnitude, alguns erros de cache nível 2 podem ter um impacto de desempenho significativos.

O segundo caso de quando importa (alguns) consumo de memória é durante a inicialização a frio do aplicativo. Como mostra a Figura 1 , o acesso ao disco rígido é muito mais lento que o acesso de memória principal. O sistema operacional tenta atenuar isso pelo cache de dados do disco na memória principal. Esse é o motivo que um aplicativo é mais rápido quando iniciado na segunda vez, durante o que é chamado inicialização a quente (os dados foi em cache na memória mais rápida). Para a primeira inicialização (fria), o cache não ainda aconteceu e dados tem que ser encontrado a partir de disco. A única maneira de melhorar isso é carregar menos dados do disco. Somente a memória obtida a partir do disco (como as instruções de programa) afeta a inicialização a frio; memória inicializada pelo próprio programa, incluindo todos os dados na heap e pilha, não afeta a inicialização a frio.

O caso final quando for assuntos de consumo de memória durante a troca de aplicativo. Quando seu aplicativo é razoavelmente grande (maior do que 50 MB) e um usuário alterna com outros aplicativos, esses aplicativos roubar a memória física do seu aplicativo. Quando o usuário retorna ao seu aplicativo, essas páginas roubadas precisa ser obtidas volta de disco, que torna seu aplicativo muito lento. Isso é semelhante a ocorrência de inicialização a frio, exceto que ela afeta não apenas instruções de programa, mas toda a memória — incluindo memória que foi inicializada pelo seu aplicativo. Como servidores executar muitos programas não relacionados ao mesmo tempo e continuamente, os servidores são aplicativo alternar constantemente; isso significa que a memória é quase sempre um problema para servidores.

O que pode ser concluído?

Se o código poderia ser reorganizado magicamente para garantir que todas as solicitações de memória foram certificadas nos caches rápidos, o programa poderia acelerar consideravelmente. Na prática, isso é possível somente em circunstâncias incomuns porque normalmente algoritmos do programa determinam a ordem dos acessos de memória. Uma técnica mais viável é minimizar a quantidade de memória usada. Isso reduz a carga nos caches rápidas e torna o programa mais rápidas. Para dados estruturas com acessados com freqüência (ativo) partes que não caibam nos caches da CPU (normalmente eles deve ser mais do que vários megabytes), uma redução de 30 % no tamanho da memória de acesso de dados geralmente resulta em uma melhora de 10 % na velocidade da CPU.

Memória pode ser reduzida de três maneiras. Primeiro, você pode executar menos código (que ajuda a inicialização a frio). Isso se aplica aos casos óbvios em que algo foi computado incorretamente em primeiro lugar. Em segundo lugar, você pode tocar menos dados. Isso é semelhante à estratégia de primeira, mas ele se aplica às estruturas de dados envolvidas. Por fim (e talvez o mais comumente), a estrutura de dados pode ser codificada de maneira diferente, reduzindo seu tamanho, ou separando fisicamente (pequeno) normalmente acessados dados da parte uncommonly acessado (grande).

Normalmente, essas técnicas precisar fazer uma alteração na representação dos dados e exigem alterações para um grande número de sites de código para implementar. Portanto, é muito mais fácil fazer essas alterações mais cedo no ciclo de desenvolvimento, portanto, vale a pena pensar em memória mais cedo!

O Gerenciador de tarefas

A primeira etapa na redução o consumo de memória do seu aplicativo é entender o quanto ele está sendo usado. Para que você pode usar o aplicativo Gerenciador de tarefas interno do Windows.

A maioria dos usuários estiver familiarizado com o Gerenciador de tarefas. Você pode chamá-la digitando taskmgr na sua janela de comando de execução (winkey + R), ou pressionando CTRL-ALT-DEL e selecionando "Iniciar o Gerenciador de tarefas". Na guia "processos", você encontrará informações sobre processos atualmente em execução completo do sistema. Se as colunas não incluem PID, conjunto de trabalho de memória e conjunto de trabalho particular de memória, use o modo de exibição | opção de menu dos selecionar colunas para adicioná-los à exibição.

Compartilhado versus memória não compartilhada

O conjunto de trabalho é a memória física atualmente sendo usada pelo processo. No entanto, o sistema de operacional executa otimizações para garantir que toda a memória não seja igualmente cara. Grande parte a memória que um processo usa contém dados somente leitura (por exemplo, as instruções reais para executar). Porque esses dados são somente leitura podem ser compartilhado entre todos os processos que precisam dele. Desde que todos os processos fazer amplo uso de código de sistema compartilhado, somente leitura, uma quantidade considerável de conjunto de trabalho de cada processo é compartilhada. Assim, o conjunto de trabalho total tende a overestimate significativamente o custo true da memória usada por um processo.

O sistema operacional também controla de memória (particular) não compartilhada. Isso inclui todos os memória de leitura / gravação usada pelo processo. Enquanto o conjunto de trabalho particular underestimates o custo true de memória usada por um processo (veremos como quando discutiremos a ferramenta VADump), ele tende a ser uma melhor métrica para otimizar, porque diferentemente Otimizar memória compartilhada, os ganhos na memória particular reduzirá a pressão de memória total no computador.

Finalmente, o total particular memória e contagens de perder uma importante memória usada por um processo: o cache do sistema de arquivos. Como o acesso ao disco rígido é tão caro, mesmo quando os dados do arquivo não estão mapeados diretamente na memória, ele é armazenada em cache pelo sistema operacional. Esse uso de memória aumenta a pressão de memória no sistema e não está incluído em uma das métricas de conjunto de trabalho (ele pertencente o sistema operacional). Não há muito que pode ser feito sobre o acesso de arquivo (se o programa precisar de um arquivo, ele não pode ser evitado), para que ele pode ser considerado um custo que não pode ser otimizado.

Tamanho do aplicativo

Um aplicativo pode ser categorizado como pequeno, médio ou grande, dependendo do seu uso de memória. Um pequeno aplicativo tem um 20 MB ou menor trabalho definir tamanho com um menor que 5 MB particular o conjunto de trabalho; um aplicativo de médio porte possui um trabalho de definir o tamanho do aproximadamente 50 MB com o cerca de 20 MB conjunto de trabalho particular; um aplicativo grande normalmente tem trabalho conjunto tamanhos maiores de 100 MB, com tamanhos de conjunto de trabalho particular exceder 50 MB. Quanto maior for o seu aplicativo, o mais valiosos otimizar o uso da memória do seu aplicativo é provavelmente será.

Uma maneira simples e fácil para monitorar o uso da memória e verificação de vazamentos é executar um teste de detecção em seu aplicativo. Executar o aplicativo para um pouco e monitorar o uso do conjunto de trabalho; se o conjunto de trabalho aumenta acoplado, que pode significar um vazamento de memória ou outros problemas.

VADump: uma exibição mais detalhada

O Gerenciador de tarefas fornece apenas um resumo do uso de memória de um aplicativo. Para obter mais detalhes você precisa uma ferramenta chamada VADump (consulte a barra lateral Recursos). Isso é chamado digitando VADump –sop ProcessID no prompt de comando sob o diretório no qual VADump está instalado. Imprime uma divisão de memória em um único processo para nível DLL de granularidade. Um despejo típico é mostrado na Figura 2 .

fig02.gif

A Figura 2 saída VADump aberto no Bloco de notas

Para ler o despejo, comece com o conjunto de trabalho total geral. Esse número deve concordar com o número no Gerenciador de tarefas. Esse número é dividido em categorias de oito. Mais interessantes essas categorias são:

  • Dados de código/estática, que representa as DLLs que foram carregados pelo processo.
  • Heap, que nativo representa (não GC) pilha de memória usada.
  • Outros dados, que representa a memória alocada usando a função VirtualAlloc do sistema operacional. Para código gerenciado isso é importante porque ele inclui a pilha de coleta de lixo inteira.

Recursos de desempenho

Despejo de endereço virtual:

go.microsoft.com/fwlink/?LinkId=149683

Blog da equipe CLR Perf (instruções em investigando suspeitos carregamentos DLL):

blogs.msdn.com/clrperfblog

Blog da equipe do VS Profiler:

blogs.msdn.com/Profiler

Melhorar o desempenho de aplicativos .NET e escalabilidade:

msdn.microsoft.com/library/ms998530

Blog de desempenho do Windows: investigações usando Xperf:

blogs.msdn.com/pigscanfly/

Blog do Vance Morrison:

blogs.msdn.com/vancem

Blog do rico Mariani:

blogs.msdn.com/ricom

Lutz Roeder .NET Reflector para inspecionar o código:

blog.lutzroeder.com

Tudo sobre o CLR - investigar problemas de memória:

msdn.microsoft.com/Magazine/cc163528

A memória utilizada pelas DLLs é mais dividida por VADump após a tabela resumida. Para cada DLL, ele mostra o número de páginas (uma página é sempre 4 k) que usa cada DLL. Portanto, uma possível determinar o custo de memória de todo o código que é carregado.

Na Figura 2 , é uma linha rotulada "conjunto de trabalho total geral". O total de trabalho conjunto em kilobytes e páginas é na primeira coluna. Colunas 2, 3 e 4 (particular Kbytes, Kbytes compartilháveis e SharedKBytes) somar para a coluna total conjunto de trabalho. É coluna 2, o valor de Kbytes particular, que é descrito como conjunto de trabalho particular em TaskManager, enquanto a coluna 1 é mostrada como total de trabalho definir em TaskManager. Assim, VADump permite que você consulte a separação entre conjuntos de trabalho particular e o total, inclusive conjuntos de trabalho compartilháveis e compartilhado. Isso é uma imagem mais completa que o que está disponível por meio de TaskManager.

Quando aplicativos .NET são grandes, eles são normalmente grandes ou porque eles executar muito código ou eles usam uma grande quantidade de dados.

Nesse caso, você verá um grande número de DLLs carregadas e a contribuição de dados de código/estática tende a dominam o total do conjunto de trabalho. Para aplicativos gerenciados, esses dados está no heap GC e, portanto, aparecendo como outros dados dominar o conjunto de trabalho.

Na parte inferior da Figura 2 , você vê módulo trabalhando conjuntos (páginas) listados. Isso informa a você quais módulos contribuem para o conjunto de trabalho do aplicativo e cada módulo do conjunto de quanto trabalho está consumindo. Assim, você pode rapidamente determinar quanto conjunto de trabalho uma DLL específica contribui em termos de conjunto de trabalho particular, compartilhado e compartilhável a DLL. Este modo de exibição sem ambigüidade mostra se uma carga DLL pode ser eliminada e quantos bytes de conjunto de trabalho particular podem ser shaved fora do conjunto de trabalho do aplicativo.

Uma vez uma DLL que pode não ser pagamento para a reprodução é identificada (por exemplo, uma DLL pode ser carregada mesmo se não for usada em uma execução específica), a próxima etapa é identificar por que a DLL específica é carregada e tentar eliminar uma carga indesejada. As etapas para investigar suspeitos carregamentos DLL pode ser encontrado noBlog de Perf do Framework e do CLR.

Os dados de heap que são mostrados pela saída de VADump são para a heap não gerenciada — trata a memória que não será gerenciada pelo .NET GC. É importante manter esse número menor para o GC pode gerenciar a maior parte da sua memória, limpando conforme necessário.

A categoria de outros dados representa chamadas para uma primitiva OS memória alocação função (VirtualAlloc) que não é possível categorizar VADump de qualquer outra forma. Para aplicativos. NET, normalmente o componente mais importante de outros dados é a pilha de coleta de lixo que contém todos os objetos definidos pelo usuário.

O coletor de lixo do .NET

O runtime do .NET oferece suporte a gerenciamento automático de memória. Ele controla cada alocação de memória feita pelo programa gerenciado e periodicamente chama um GC que encontra a memória que não está mais em uso e reutiliza-lo para alocações de novas. Uma otimização importante que o coletor de lixo executa é que ele não pesquisa sempre a pilha inteira, mas partições a pilha em três gerações (0, 1 e 2).

Geração 0 é o menor desses e geralmente leva apenas 1/10 de um milissegundo a aparência completa, mas apenas para limpar as alocações que ocorreu após o último GC e (obviamente, não estão sendo usadas). O ideal é que o tamanho de uma geração é menor que o tamanho do cache L2. Geração 1 CGs lidar com as alocações que sobreviveram um GC; isso levará mais tempo para executar de Gen 0 CGs, levando aproximadamente 1 milissegundos. O ideal é que deve haver 10 CGs Gen 0 para cada Gen 1 GC.

Gen 2 CGs lidar com todos os objetos. Portanto, o tempo gasto pode ser significativo. Por exemplo, pode levar aproximadamente 160 em milissegundos para uma pilha de 20 MB, que é uma quantidade considerável de tempo. O tempo aproximadamente aumenta linearmente com o tamanho do heap (milissegundos aproximadamente 8 por MB como uma estimativa bastante aproximada). O custo true depende da quantidade de memória sobreviver, o número de ponteiros de GC no sobreviver a memória e quão fragmentada a pilha está. O ideal é que deve haver 10 Gen 1 CGs para cada Gen 2 GC.

Feito em sua totalidade, heap .NET GC parece um sawtooth com os troughs correspondente a coletas de Gen 2, conforme mostrado na Figura 3 . A proporção de heap para trough de Gen 2 típica é aproximadamente 1,6, com a taxa de sendo amplamente independentes do tamanho do heap (com não fragmentação). Na presença de fragmentação, esse número pode variar significativamente.

fig03.gif

A Figura 3 forma de onda Sawtooth heap de GC

PerfMon

VADump fornece o primeiro nível de divisão do uso de memória no processo. No entanto, ele não precisamente dizer quanta memória do GC estamos usando (a categoria de outros dados pode incluir memória diferente de heap GC), e ele não informar se temos uma taxa de integridade de gerações de GC. Para isso é necessário usar o aplicativo de Windows PerfMon. Você pode iniciá-lo digitando PerfMon na janela de comando de execução que deve abrir a janela mostrada na Figura 4 . PerfMon é capaz de obter uma grande quantidade de dados de desempenho, mas aqui vamos nos concentrar em seu uso para monitorar o heap de GC.

fig04.gif

A Figura 4 tela de inicialização de PerfMon

fig05.gif

A Figura 5 Selecionar contadores para monitorar em PerfMon

Após o PerfMon trata de backup, é necessário configurá-lo para exibir informações sobre o GC. Fazemos isso primeiro clicando no item de monitor de desempenho no controle de árvore no painel esquerdo. Isso altera o painel direito para exibir dados do contador de desempenho. Agora, clique na + assinar adicionando novos contadores. Em seguida, selecione os contadores que você deseja monitorar, bem como os processos que quiser assistir, como mostrado na Figura 5 .

Quando você seleciona alguns contadores, você irá notar nomes de todos os aplicativos usando o tempo de execução. Você pode selecionar um ou dois ou Entretanto muitos aplicativos que deseja monitorar. Além disso, há uma instância todas as instâncias nomeada é permitir que dados de monitoramento em todas as instâncias mostradas, mas os dados serão exibidos separadamente. Além disso, há uma instância de _Global_, que soma os dados de instâncias diferentes.

Se um aplicativo foi iniciado depois PerfMon foi sendo usado para monitorar outros aplicativos já, um pode adicionar mais aplicativos clicando na + assinar e adicionar contadores para o novo aplicativo (apenas adicionar a nova instância é necessária; as outras instâncias continuará a ser exibida em PerfMon).

Finalmente, por padrão os dados são exibidos graficamente, mas é mais útil para exibi-la numericamente. Isso pode ser feito clicando na barra de ferramentas relatório-tipo ( Figura 6 ). Em um teste, ele mostrou-nos que 7.3MB fora do total que 8.6MB do conjunto de trabalho particular foi ocupam pelo heap GC e aproximadamente 11 % de tempo foi gasto no GC. Um número íntegro para o tempo em GC é menor que 10 % do total de aplicativo tempo, portanto, neste aplicativo específico deve ser on the beira. Finalmente, ele também nos informa o número de coleções de Gen0, Gen1 e Gen2. O ideal é que queremos que o número de coletas Gen0 a ser pelo menos 10 vezes de coletas Gen1 e o número de coletas Gen1 a ser pelo menos 10 vezes de coleções de Gen2.

fig06.gif

A Figura 6 Exibir relatório de PerfMon

Conclusão

Problemas de memória são notoriamente difíceis de depurar. Se seu aplicativo for grande o suficiente para preocupam a memória, a chave é limitar o uso de memória nos primeiros estágios de desenvolvimento. Noções básicas sobre como memória de aplicativo é dividida é a primeira etapa nesse processo; monitorar o uso de memória de aplicativo é próxima. Execute que com identificando oportunidades fazendo perguntas sobre quais DLLs contribuir com o máximo para o consumo de memória, por que do GC Gen2 estão ocorrendo portanto, com freqüência, são sua memória alocações pagamento de execução e assim por diante. Em seguida, otimizar o uso de memória adequadamente. Se você tirar imediatamente apenas uma lição, deveria ser que o tempo gasto mais cedo no ciclo de desenvolvimento sobre problemas de memória paga para si mesma posteriormente, para que ele realmente paga pensar em memória mais cedo!

Envie suas dúvidas e comentários paraclrinout@microsoft.com.

Subramanian Ramaswamy é o gerente de programa do desempenho de CLR na Microsoft. Ele contém um PhD em elétrica e de computador engenharia da Geórgia Institute of Technology.

Vance Morrison é o arquiteto do parceiro e o Gerenciador de grupo para desempenho de CLR na Microsoft. Ele conduziu o design do .NET Intermediate Language (IL) e ele foi envolvido .NET desde seu início.