CLR
Uma visão geral dos aprimoramentos de desempenho no .NET 4.5
Ashwin Kamath
Este artigo discute a versão de pré-lançamento do Microsoft .NET Framework 4.5. Todas as informações relacionadas estão sujeitas a alterações.
Na equipe do Microsoft .NET Framework, sempre entendemos que melhorar o desempenho é no mínimo tão importante para os desenvolvedores quanto acrescentar novos recursos de tempo de execução e APIs de biblioteca. O .NET Framework 4.5 inclui investimentos consideráveis em desempenho, o que beneficia todos os cenários de aplicativo. Além disso, como o .NET 4.5 é uma atualização do .NET 4, até mesmo os seus aplicativos .NET 4 podem aproveitar muitos dos aprimoramentos de desempenho feitos em recursos já existentes naquela versão.
Quando os desenvolvedores precisam oferecer experiências satisfatórias com aplicativos, aspectos como tempo de inicialização (consulte msdn.microsoft.com/magazine/cc337892), uso da memória (consulte msdn.microsoft.com/magazine/dd882521), taxa de transferência e capacidade de resposta são de fato muito importantes. Estabelecemos metas para melhorar essas métricas em diferentes cenários de aplicativo e planejamos as alterações necessárias para atingi-las ou superá-las. Neste artigo, apresentarei uma visão geral de alguns dos principais aprimoramentos de desempenho que fizemos no .NET Framework 4.5.
CLR
Nesta versão, nos concentramos em: explorar os vários núcleos de processador para melhorar o desempenho, reduzir a latência no coletor de lixo e aperfeiçoar a qualidade do código de imagens nativas. Veja a seguir alguns dos principais recursos com aprimoramentos de desempenho.
JIT (Just-in-Time) de vários núcleos Monitoramos continuamente os avanços em hardware de baixo desempenho e trabalhamos com fornecedores de chips para obter o melhor desempenho assistido por hardware. Especificamente, temos tido chips de vários núcleos em nossos laboratórios de desempenho desde quando eles foram disponibilizados e fizemos alterações apropriadas para explorar essa mudança de hardware em particular, mas inicialmente essas alterações beneficiaram apenas alguns clientes.
Hoje, quase todo PC tem pelo menos dois núcleos, e os novos recursos que exigem mais de um núcleo são muito úteis de imediato. No início do desenvolvimento do .NET 4.5, determinamos se era razoável utilizar vários núcleos de processador para compartilhar a tarefa de compilação JIT, especificamente como parte da inicialização do aplicativo, para acelerar a experiência como um todo. Como parte dessa investigação, descobrimos que aplicativos gerenciados suficientes têm um número limite mínimo de métodos compilados por JIT que justificam o investimento.
O recurso funciona fazendo a compilação JIT de métodos que provavelmente serão executados em um thread em segundo plano, que em um computador com vários núcleos executará em outro núcleo, em paralelo. No exemplo ideal, o segundo núcleo rapidamente avança na execução principal do aplicativo, por isso a maioria dos métodos são compilados por JIT até serem necessários. Para saber quais métodos devem ser compilados, o recurso gera dados de perfil que controlam os métodos executados e se orienta por esses dados em uma execução posterior. Esse requisito de gerar dados de perfil é a principal maneira de interagir com esse recurso.
Adicionando um mínimo de código, é possível usar esse recurso do tempo de execução para melhorar significativamente os tempos de inicialização de aplicativos cliente e de sites. Em particular, você precisa fazer chamadas diretas a dois métodos estáticos na classe ProfileOptimization, no namespace System.Runtime. Consulte a documentação do MSDN para obter mais informações. Esse recurso é habilitado por padrão para aplicativos ASP.NET 4.5 e aplicativos Silverlight 5.
Imagens nativas otimizadas Durante várias versões, permitimos a pré-compilação de código para imagens nativas através de uma ferramenta chamada geração de imagem nativa (NGen, Native Image Generation). As imagens nativas resultantes normalmente geravam inicializações de aplicativos mais rápidas do que observado na compilação JIT. Nesta versão, introduzimos uma ferramenta complementar chamada Otimização Gerenciada Orientada por Perfis (MPGO, Managed Profile Guided Optimization), que otimiza o layout de imagens nativas para melhorar ainda mais o desempenho. A MPGO usa uma tecnologia de otimização orientada por perfis, conceitualmente muito parecida com o JIT de vários núcleos descrito anteriormente. Os dados de perfil do aplicativo incluem um cenário ou um conjunto de cenários representativo, que pode ser usado para reorganizar o layout de uma imagem nativa de forma que os métodos e outras estruturas de dados necessárias na inicialização sejam colocados densamente dentro de uma parte da imagem nativa, resultando em tempo de inicialização mais curto e menos trabalho (uso da memória de um aplicativo). De acordo com os nossos testes e experiência, normalmente vemos um benefício da MPGO com aplicativos gerenciados maiores (por exemplo, aplicativos grandes com interface gráfica do usuário interativa) e recomendamos seu amplo uso neste artigo.
A ferramenta MPGO gera dados de perfil para uma DLL de linguagem intermediária (IL, Intermediate Language) e adiciona o perfil como um recurso para a DLL IL. A ferramenta NGen é utilizada para pré-compilar DLLs IL após a criação de perfil e executa otimização adicional devido à presença dos dados de perfil. A Figura 1 mostra o fluxo do processo.
Figura 1 Fluxo do processo com a ferramenta MPGO
Alocador da pilha de objetos grandes (LOH, Large Object Heap) Muitos desenvolvedores do .NET solicitaram uma solução para o problema da fragmentação da LOH ou uma maneira de forçar a compactação da LOH. Você pode aprender mais sobre o funcionamento da LOH na coluna Tudo sobre CLR, de Maoni Stephens, na edição de junho de 2008, que está disponível em msdn.microsoft.com/magazine/cc534993. Resumindo, qualquer objeto com tamanho de 85.000 ou mais é alocado na LOH. Atualmente, a LOH não é compactada. A compactação seria muito demorada porque o coletor de lixo teria de mover objetos grandes e, por isso, é uma proposta cara. Quando objetos da LOH são alocados, deixam espaços livres entre os objetos que sobrevivem à coleta, o que leva ao problema da fragmentação.
Esclarecendo um pouco mais, o CLR cria uma lista livre com base nos objetos inativos, permitindo que sejam reutilizados posteriormente para atender a solicitações de alocação de objetos grandes; os objetos inativos adjacentes são transformados em um objeto livre. No futuro, um programa poderá acabar em uma situação em que esses fragmentos de memória livre entre objetos ativos grandes não sejam grandes o suficiente para fazer mais alocações de objetos na LOH, e como a compactação não é uma opção, rapidamente nos vemos diante de um problema. Isso faz com que os aplicativos não respondam e, futuramente, leva a exceções por memória insuficiente.
No .NET 4.5, fizemos algumas alterações que permitem o uso eficiente de fragmentos de memória na LOH, especificamente no que diz respeito à maneira como gerenciamos a lista livre. As alterações se aplicam à coleta de lixo (GC, Garbage Collection) de estações de trabalho e servidores. Isso não altera o limite de 85.000 bytes para objetos da LOH.
GC em segundo plano para servidor No .NET 4, ativamos a GC em segundo plano para GC para estação de trabalho. Desde então, temos visto com mais frequência computadores cujos tamanhos de pilha de extremidade superior variam de alguns gigabytes para dezenas de gigabytes. Até mesmo um coletor paralelo otimizado como o nosso pode demorar segundos para coletar essas pilhas grandes, bloqueando os threads de aplicativos durante segundos. A GC em segundo plano para servidores introduz suporte para coletas simultâneas ao nosso coletor de servidor. Ela minimiza as coletas de bloqueio demoradas e mantém a alta produtividade dos aplicativos.
Se você estiver usando a GC para servidores, não precisará fazer nada para aproveitar as vantagens desse novo recurso; a GC em segundo plano para servidores ocorrerá automaticamente. As características de alto nível da GC em segundo plano são as mesmas da GC para cliente e servidor:
- Somente a GC completa (geração 2) pode ocorrer em segundo plano.
- A GC em segundo plano não faz compactação.
- A GC em primeiro plano (GC das gerações 0 e 1) pode ocorrer durante a GC em segundo plano. A GC para servidor é feita em threads de GC de servidor dedicado.
- A GC de bloqueio completo também ocorre em threads de GC de servidor dedicado.
Programação assíncrona
Um novo modelo de programação assíncrona foi introduzido como parte da CTP assíncrona do Visual Studio e agora é um elemento importante do .NET 4.5. Esses novos recursos de linguagem do .NET 4.5 permitem que você escreva código assíncrono de modo produtivo. Duas novas palavras-chave de linguagem do C# e do Visual Basic, chamadas “async” e “await”, permitem isso nesse novo modelo. O .NET 4.5 também foi atualizado para dar suporte a aplicativos assíncronos que usam essas novas palavras-chave.
O portal de Programação Assíncrona do Visual Studio no MSDN (msdn.microsoft.com/vstudio/async) é um ótimo recurso para obter amostras, white papers e palestras sobre os novos recursos e suporte de linguagem.
Bibliotecas de computação paralela
Diversos aprimoramentos foram feitos nas bibliotecas de computação paralela (PCLs, Parallel Computing Libraries) do .NET 4.5 para melhorar as APIs atuais.
Tarefas mais leves e mais rápidas As classes System.Threading.Tasks.Task e Task<TResult> foram otimizadas para usar menos memória e executar mais rapidamente em cenários-chave. Especificamente, casos relacionados à criação de tarefas e ao planejamento de continuações observaram melhorias de desempenho de até 60%.
Mais consultas PLINQ são executadas em paralelo O PLINQ retorna à execução em sequência se considera que seria mais prejudicial (tornaria as coisas mais lentas) paralelizando uma consulta. Essas decisões são estimativas e nem sempre perfeitas e, no .NET 4.5, o PLINQ reconhecerá mais classes de consultas que poderá paralelizar com êxito.
Coletas simultâneas mais rápidas Foram feitos inúmeros ajustes em System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue> para torná-la mais rápida para determinados cenários.
Para obter mais detalhes sobre essas alterações, visite o blog da equipe da plataforma de computação paralela, em blogs.msdn.com/b/pfxteam.
ADO.NET
Suporte para linha de compactação de bit nulo Dados nulos são especialmente comuns para clientes que aproveitam o recurso de colunas esparsas do SQL Server 2008. Os clientes que usam esse recurso podem produzir conjuntos de resultados que contêm muitas colunas nulas. Para esse cenário, foi introduzida a compactação de bit nulo de linha (token SQLNBCROW ou, simplesmente, NBCROW). Ela diminui o espaço utilizado pelas linhas de conjunto de resultados enviadas do servidor com muitas colunas compactando várias colunas com valores NULL em uma máscara de bits. Isso ajuda consideravelmente na compactação de dados do protocolo TDS onde existem muitas colunas nulas nos dados.
Entity Framework
Consultas LINQ compiladas automaticamente Hoje, quando você escreve uma consulta LINQ to Entities, o Entity Framework percorre a árvore de expressões gerada pelo compilador do C#/Visual Basic e traduz (ou compila) isso em SQL, como ilustrado na Figura 2.
Figura 2 Uma consulta LINQ to Entities convertida em SQL
Porém a compilação da árvore de expressão em SQL envolve uma certa sobrecarga, principalmente no caso de consultas mais complexas. Em versões anteriores do Entity Framework, quando você queria evitar essa penalidade de desempenho sempre que uma consulta LINQ era executada, tinha de usar a classe CompiledQuery.
Essa nova versão do Entity Framework dá suporte a um novo recurso chamado Consultas LINQ compiladas automaticamente. Agora, toda consulta LINQ to Entities que você executa é automaticamente compilada e colocada no cache de plano de consulta do Entity Framework. Quando você executar a consulta de novo, o Entity Framework a encontrará no cache de consultas e não terá de repetir todo o processo de compilação. Saiba mais sobre isso em bit.ly/iCaM2b.
Windows Communication Foundation e Windows Workflow Foundation
A equipe do Windows Communication Foundation (WCF) e do Windows Workflow Foundation (WF) também fez uma série de aprimoramentos de desempenho nesta versão, como os seguintes:
- Aprimoramentos na escalabilidade de ativação de TCP: os clientes relataram um problema na ativação de TCP: quando muitos usuários simultâneos enviam solicitações com reconexões constantes, o serviço de compartilhamento da porta TCP não apresenta boa escalabilidade. Isso foi corrigido no .NET 4.5.
- Suporte interno a compactação GZip para HTTP/TCP do WCF: com essa nova compactação, esperamos uma taxa de compactação de até cinco vezes.
- Reciclagem de host quando o uso de memória é alto para o WCF: quando o uso de memória é alto (botão configurável), usamos a lógica LRU (Least Recently Used, menos utilizado recentemente) para reciclar serviços do WCF.
- Suporte a streaming assíncrono de HTTP para WCF: implementamos esse recurso no .NET 4.5 e obtivemos a mesma taxa de transferência obtida com o streaming síncrono, mas com escalabilidade bem melhor.
- Melhorias em fragmentação de geração 0 para TCP do WCF.
- BufferManager otimizado para WCF para objetos grandes: no caso de objetos grandes, um pool de buffers melhor foi implementado para evitar altas despesas com GC de geração 2.
- Aprimoramentos na validação WF com cache de expressões: esperamos aprimoramentos de até três vezes em um cenário básico de carregamento e execução de WF.
- Implementação do Rastreamento de Eventos para Windows (ETW) completo do WCF/WF: apesar de não ser um recurso de aprimoramento de desempenho, ajuda os clientes em investigações de desempenho.
Mais detalhes estão disponíveis no blog da Equipe do Workflow, em blogs.msdn.com/b/workflowteam, e no artigo da Biblioteca MSDN, em bit.ly/n5VCtU.
ASP.NET
Melhorar a densidade de sites (também definido como “consumo de memória por site”) e o tempo de inicialização a frio de sites no caso de hospedagem compartilhada foram duas metas de desempenho importantes da equipe do ASP.NET para o .NET 4.5.
Em cenários de hospedagem compartilhada, muitos sites compartilham o mesmo computador. Em ambientes como esses, geralmente há pouco tráfego. Dados fornecidos por algumas empresas de hospedagem mostram que, na maior parte do tempo, as solicitações por segundo ficam abaixo de 1 rps, com picos ocasionais de 2 rps ou mais. Isso significa que muitos processo de trabalho provavelmente serão anulados se ficarem ociosos por muito tempo (por padrão, 20 minutos no IIS 7 e em versões posteriores). Por isso, o tempo de inicialização torna-se muito importante. No ASP.NET, esse é o tempo necessário para um site receber e responder a uma solicitação, comparando o tempo durante o qual o processo de trabalho ficou desativado com o tempo em que o site já estava compilado.
Implementamos diversos recursos nesta versão para melhorar o tempo de inicialização nos cenários de hospedagem compartilhada. Os recursos utilizados são os seguintes:
- Confinando assemblies Bin (compartilhamento de assemblies comuns): o recurso de cópia de sombra do ASP.NET permite que os assemblies utilizados em um domínio de aplicativo sejam atualizados sem descarregar AppDomain (necessário porque o CLR bloqueia os assemblies que estão sendo usados). Isso é feito copiando assemblies de aplicativo para um local à parte (um local padrão determinado pelo CLR ou um local especificado pelo usuário) e carregando os assemblies desse local. Desse modo, o assembly original pode ser atualizado enquanto a cópia de sombra está bloqueada. O ASP.NET ativa esse recurso por padrão para assemblies da pasta Bin, para que as DLLs continuem sendo atualizadas enquanto o site está funcionando.
- O ASP.NET reconhece a pasta Bin de um site como sendo uma pasta especial de assemblies compilados (DLLs) para controles ASP.NET personalizados, componentes ou outro código que precisa ser referenciado em um aplicativo ASP.NET e compartilhado entre várias páginas do site. Um assembly compilado da pasta Bin é referenciado automaticamente em todas as partes do aplicativo Web. O ASP.NET também detecta a versão mais recente de uma determinada DLL na pasta Bin para uso pelo site. Os aplicativos previamente incluídos a serem utilizados por sites ASP.NET normalmente são instalados na pasta Bin e não no Cache de Assembly Global.
- As equipes do ASP.NET e do CLR descobriram que, quando muitos sites residem no mesmo servidor e usam o mesmo aplicativo, muitas dessas DLLs de cópia de sombra tendem a ser exatamente iguais. Como esses arquivos são lidos do disco e carregados na memória, isso gera muitas cargas redundantes que aumentam o tempo de inicialização e o consumo de memória. Trabalhamos no uso de links simbólicos que o CLR deverá seguir e implementamos a identificação dos arquivos comuns e os confinamos em um local especial (que será apontado pelos links simbólicos). O ASP.NET configura a cópia de sombra automaticamente para as DLLs de Bin. Agora os hosters compartilhados podem configurar seus computadores de acordo com as diretrizes do ASP.NET para obter o benefício de desempenho máximo.
- JIT de vários núcleos: consulte as informações relacionadas na seção “CLR” anterior. A equipe do ASP.NET usa o recurso de JIT de vários núcleos para melhorar o tempo de inicialização distribuindo a compilação JIT entre os núcleos do processador. Por padrão, isso é possível no ASP.NET, assim você aproveita as vantagens desse recurso sem nenhum trabalho extra. Você pode desativar esse recurso usando a seguinte configuração no arquivo web.config:
<configuration>
<!-- ... -->
<system.web>
<compilation profileGuidedOptimizations="None" />
<!-- ... -->
- Prefetcher: a tecnologia Prefetcher do Windows é muito eficaz para reduzir o custo de leitura de disco da paginação durante a inicialização de aplicativos. Agora o Prefetcher também está ativado, mas não por padrão, no Windows Server. Para ativar o Prefetcher para hospedagem Web de alta densidade, execute o seguinte conjunto de comandos na linha de comando:
sc config sysmain start=auto
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters" /v EnablePrefetcher /t REG_DWORD /d 2 /f
reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Prefetcher" /v MaxPrefetchFiles /t REG_DWORD /d 8192 /f
net start sysmain
- Em seguida, você poderá atualizar o arquivo web.config para usá-lo no ASP.NET:
<configuration>
<!-- ... -->
<system.web>
<compilation enablePrefetchOptimization
="true" />
<!-- ... -->
- Ajuste da GC para hospedagem Web de alta densidade: a GC pode afetar o consumo de memória de um site, mas é possível ajustá-la para permitir um melhor desempenho. Você pode ajustar ou configurar a GC para melhor desempenho de CPU (diminuindo a frequência de coletas) ou menor consumo de memória (ou seja, coletas mais frequentes para liberar a memória antes). Para ativar o ajuste da GC, selecione a configuração HighDensityWebHosting no arquivo aspnet.config da pasta Windows\Microsoft\v4.0.30319 para obter menor consumo de memória (conjunto de trabalho) por site:
<configuration>
<!-- ... -->
<runtime>
<performanceScenario
value="HighDensityWebHosting" />
<!-- ... -->
Mais detalhes sobre os aprimoramentos de desempenho feitos no ASP.NET estão disponíveis no white paper “Introdução à próxima versão do ASP.NET”, em bit.ly/A66I7R.
Envie seus comentários
A lista apresentada aqui não está completa. Existem mais alterações secundárias para melhorar o desempenho que foram omitidas a fim de manter o escopo deste artigo restrito aos principais recursos. Fora isso, as equipes de desempenho do .NET Framework também estão ocupadas trabalhando em aprimoramentos de desempenho específicos de aplicativos estilo Metro gerenciados do Windows 8. Depois que você baixar e experimentar o .NET Framework 4.5 e o Visual Studio 11 beta para Windows 8, envie seus comentários ou sugestões para as próximas versões.
Glossário de termos
Hospedagem compartilhada: também conhecida como “hospedagem Web compartilhada”, a hospedagem Web de alta densidade permite que centenas, se não milhares, de sites sejam executados no mesmo servidor. Compartilhando os custos de hardware, é possível manter cada site por um custo mais baixo. Essa técnica tem diminuído consideravelmente os obstáculos à entrada de proprietários de sites.
Inicialização a frio: inicialização a frio é o tempo necessário para iniciar um aplicativo que ainda não estava presente na memória. A inicialização a frio pode ocorrer quando você inicia um aplicativo após uma reinicialização do sistema. No caso de aplicativos grandes, a inicialização pode levar muitos segundos, pois as páginas necessárias (código, dados estáticos, Registro etc.) não estão presentes na memória e acessos ao disco são necessários para colocá-las na memória.
Inicialização a quente: inicialização a quente é o tempo necessário para iniciar um aplicativo que já está presente na memória. Por exemplo, se um aplicativo foi iniciado alguns segundos antes, é provável que a maioria das páginas já estejam carregadas na memória e o SO as reutilizará, economizando tempo precioso de acesso ao disco. Por isso é muito mais rápido inicializar um aplicativo quando você o executa pela segunda vez (e também é por isso que um segundo aplicativo .NET inicia mais rapidamente do que o primeiro porque partes do .NET já estarão carregadas na memória).
Geração de imagem nativa ou NGen: refere-se ao processo de pré-compilar executáveis de linguagem intermediária (IL, Intermediate Language) em código de máquina antes do tempo de execução. Isso resulta em dois benefícios primários de desempenho. Primeiro, ele reduz o tempo de inicialização do aplicativo evitando a necessidade de compilar código no tempo de execução. Segundo, ele aprimora o uso da memória permitindo que páginas de código sejam compartilhadas por muitos processos. Também existe uma ferramenta, NGen.exe, que cria imagens nativas e as instala no Cache de Imagem Nativa (NIC, Native Image Cache) do computador local. O tempo de execução carrega imagens nativas quando disponíveis.
Otimização orientada por perfis: a otimização orientada por perfis comprovadamente melhora os tempos de inicialização e execução de aplicativos nativos e gerenciados. O Windows fornece o conjunto de ferramentas e a infraestrutura para executar a otimização orientada por perfis de assemblies nativos, enquanto o CLR fornece o conjunto de ferramentas e a infraestrutura para executar otimizações orientadas por perfis de assemblies gerenciados (chamada otimização gerenciada orientada por perfis ou MPGO). Essas tecnologias são utilizadas por muitas equipes na Microsoft para melhorar o desempenho de seus aplicativos. Por exemplo, o CLR executa otimização orientada por perfis de assemblies nativos (otimização orientada por perfis C++) e assemblies gerenciados (usando MPGO).
Coletor de lixo: o tempo de execução do .NET dá suporte ao gerenciamento automático da memória. Ele acompanha cada alocação de memória feita pelo programa gerenciado e periodicamente chama um coletor de lixo, que localiza a memória que não está mais em uso e a reutiliza para novas alocações. Uma otimização importante realizada pelo coletor de lixo é que ele não procura na pilha inteira todas as vezes, mas particiona a pilha em três gerações (geração 0, geração 1 e geração 2). Para obter mais informações sobre o coletor de lixo, leia a coluna Tudo sobre CLR de junho de 2009, disponível em msdn.microsoft.com/magazine/dd882521.
Compactação: no contexto de coleta de lixo, quando a pilha atinge um estado em que está suficientemente fragmentada, o coletor de lixo a compacta aproximando os objetos ativos uns dos outros. O principal objetivo da compactação da pilha é disponibilizar blocos de memória maiores nos quais alocar mais objetos.
Ashwin Kamath é gerente de programa na equipe do CLR do .NET e foi responsável pelos recursos de desempenho e confiabilidade do .NET Framework 4.5. No momento, está trabalhando em recursos de diagnóstico para a plataforma de desenvolvimento do Windows Phone.
Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Surupa Biswas, Eric Dettinger, Wenlong Dong, Layla Driscoll, Dave Hiniker, Piyush Joshi, Ashok Kamath, Richard Lander, Vance Morrison, Subramanian Ramaswamy, Jose Reyes, Danny Shih e Bill Wert