Tudo sobre CLR

Novos recursos e desempenho aprimorado do Silverlight 4

Andrew Pardoe

Uma das maiores alterações feitas no Silverlight 4 foi a mudança para uma nova versão do CLR referente ao mecanismo de execução de núcleo. Todas as versões do .NET Framework têm usado o mesmo CLR como núcleo, desde o Microsoft .NET Framework 2.0 ao .NET Framework 3.5 SP1. O .NET Framework 4 fez algumas alterações — até mesmo alterações muito grandes, como a fatoração do Perfil de Cliente fácil de baixar e a diminuição do tempo de inicialização ao otimizar o layout de binários nativos — mas sempre estivemos limitados pelo alto padrão de compatibilidade imposto por se tratar de uma atualização local.

Com o lançamento do .NET Framework 4, pudemos fazer alterações importantes no CLR propriamente dito e, ao mesmo tempo, manter a compatibilidade com versões anteriores. O Silverlight 4 usa o novo CLR como a base de seu CoreCLR e traz todos os seus aprimoramentos do desktop para a Web. Entre alguns dos aprimoramentos de tempo de execução mais notáveis está uma alteração feita no comportamento padrão do coletor de lixo (GC) e o fato de não haver mais compilação JIT (just-in-time) de binários do Silverlight Framework todas as vezes que um programa do Silverlight é executado. Fizemos aprimoramentos em todas as classes base, inclusive melhorias no armazenamento isolado e alterações em System.IO que permitem o acesso direto ao sistema de arquivos de aplicativos do Silverlight executados com permissões elevadas.

Vamos começar com algumas informações preliminares sobre o funcionamento do GC CoreCLR.

GC gerativo

O CoreCLR usa o mesmo GC que o CLR de desktop. Trata-se de um GC gerativo, ou seja, suas operações são baseadas na heurística de que os objetos alocados mais recentemente muito provavelmente são o lixo da próxima hora de coleta. Essa heurística é perceptível em escopos pequenos: funções locais não podem ser acessadas por programa logo após o retorno da função. Geralmente essa heurística também é aplicável para escopos maiores: os programas costumam manter algum estado global em objetos que duram apenas o período de execução do programa.

Normalmente, os objetos são alocados na geração mais jovem — que chamamos de Geração 0 — e promovidos durante as coletas de lixo (se sobreviverem à coleta) para gerações mais velhas até chegarem à geração máxima (que é a Geração 2 na implementação atual do GC do CLR).

Temos outra geração no GC do CLR chamada Pilha de Objetos Grandes (LOH). Os objetos grandes (no momento, definidos como objetos com mais de 85.000 bytes) são alocados diretamente na LOH. Essa pilha é coletada ao mesmo tempo em que a Geração 2.

Sem um GC gerativo, o GC precisa inspecionar a pilha inteira para descobrir qual memória está acessível e qual memória é lixo antes de coletar a memória não utilizada. Com um GC gerativo, não precisamos verificar a pilha inteira para cada coleta. Como a duração de uma coleta está diretamente relacionada ao tamanho das gerações que estão sendo coletadas, o GC é otimizado para apenas coletar a Geração 2 (e a LOH) com menos frequência. As coletas são quase instantâneas em pilhas pequenas e mais demoradas em pilhas maiores — coletas da Geração 0 levam somente dezenas de microssegundos.

Para a maioria dos programas, a Geração 2 e a LOH são bem maiores do que a Geração 0 e a Geração 1, por isso é mais demorado inspecionar toda a memória nessas pilhas. Lembre-se de que o GC sempre coleta a Geração 0 quando coleta a Geração 1 e coleta todas as pilhas ao coletar a Geração 2. Por esse motivo, uma coleta da Geração 2 é chamada de coleta completa. Para obter mais detalhes sobre o desempenho de diferentes coletas de pilhas, consulte a coluna Tudo sobre CLR de outubro de 2009, em msdn.microsoft.com/magazine/ee309515.

GC simultâneo

O algoritmo simples para realizar a coleta de lixo é fazer com que o mecanismo de execução pause todos os threads de programa enquanto o GC faz seu trabalho. Chamamos esse tipo de coleta de coleta de bloqueio. Elas permitem que o GC movimente a memória não fixada — por exemplo, para movê-la de uma geração a outra ou compactar segmentos de memória esparsos — sem que o programa saiba que nada foi alterado. Se a memória tivesse de ser movimentada durante a execução de threads de programa, para o programa ficaria parecendo que a memória foi corrompida.

Mas parte do trabalho de uma coleta de lixo não altera a memória. Desde a primeira versão do CLR, disponibilizamos um modo de GC que faz coletas simultâneas. São coletas que executam a maior parte do trabalho de um GC completo sem ter de pausar threads de programa durante o período da coleta.

Existem várias coisas que o GC pode fazer sem alterar nenhum estado visível para o programa; por exemplo, o GC pode localizar toda a memória que pode ser acessada por programa. Os threads de programa podem continuar executando enquanto o GC inspeciona as pilhas. Antes de fazer a coleta propriamente dita, o GC só precisa descobrir o que foi alterado enquanto ele inspecionava a memória; por exemplo, se o programa alocou um novo objeto, ele precisa estar marcado como acessível. Ao final desse procedimento, o GC pede para o mecanismo de execução bloquear todos os threads, assim como ocorre em um GC não simultâneo, e continua para concluir toda a memória acessível nesse ponto.

GC em segundo plano

O GC simultâneo sempre proporcionou uma ótima experiência na maioria dos cenários, mas existe um cenário que melhoramos muito. Lembre-se de que a memória é alocada na geração mais jovem ou na LOH. As gerações 0 e 1 estão localizadas em um mesmo segmento, que chamamos de segmento efêmero, pois contém os objetos de curta duração. Quando o segmento efêmero é totalmente preenchido, o programa deixa de criar novos objetos porque não há espaço para eles ali. O GC precisa fazer uma coleta no segmento efêmero a fim de liberar espaço e permitir que a alocação continue.

O problema do GC simultâneo é que ele não pode fazer nenhuma dessas coisas enquanto ocorre uma coleta simultânea. O thread do GC não pode movimentar nenhuma memória enquanto os threads de programa estão em execução (portanto, não pode promover objetos mais velhos para a Geração 2) e, como já existe um GC em andamento, ele não pode iniciar uma coleta efêmera. Mas o GC precisa liberar memória no segmento efêmero para que o programa possa continuar. É um problema sério; os threads de programa têm de ser pausados não porque o GC simultâneo está alterando o estado visível pelo programa, mas porque o programa não pode alocar. Se uma coleta simultânea detectar que o segmento efêmero está cheio assim que encontrar toda a memória acessível, ela pausará todos os threads e fará uma compactação de bloqueio.

Este problema explica a motivação por detrás do desenvolvimento de um GC em segundo plano. Ele funciona da mesma maneira que o GC simultâneo, no sentido de que o GC sempre faz a maior parte do trabalho de coleta completa em seu próprio thread no segundo plano. A principal diferença é que ele permite que ocorra uma coleta efêmera enquanto a coleta completa colhe dados. Isso significa que os programas podem continuar executando quando o segmento efêmero está preenchido. O GC simplesmente faz uma coleta efêmera e tudo continua como esperado.

O impacto do GC em segundo plano sobre a latência do programa é considerável. Ao executar o GC em segundo plano, observamos bem menos pausas na execução do programa, e as que restaram foram mais curtas.

O GC em segundo plano é o modo padrão do Silverlight 4 e só está habilitado em plataformas Windows, porque o OS X não tem parte do suporte de SO de que o GC precisa para executar no modo de segundo plano ou simultâneo.

Aprimoramentos de desempenho do NGen

Os compiladores de linguagens gerenciadas como C# e Visual Basic não produzem diretamente um código que possa ser executado no computador do usuário. Esses compiladores geram uma linguagem intermediária, chamada MSIL, que é compilada em código executável na execução do programa através de um compilador JIT.

O uso de MSIL traz muitos benefícios, que vão desde a segurança até a portabilidade, mas há duas desvantagens em relação ao código com compilação JIT. A primeira delas é a necessidade de compilar muito código do .NET Framework para que a função Main do programa possa ser compilada e executada. Isso significa que o usuário precisa esperar o JIT antes de o programa começar a executar. A segunda é que qualquer código do .NET Framework que for usado deve ser compilado para cada programa do Silverlight em execução no computador do usuário.

O NGen ajuda nesses dois problemas. Ele compila o código do .NET Framework durante a instalação para que ele já esteja compilado quando o seu programa começar a executar. O código que não foi compilado com o NGen com frequência pode ser compartilhado entre vários programas, assim o conjunto de trabalho no computador do usuário é reduzido quando se executam dois ou mais programas do Silverlight. Para obter mais informações sobre como o NGen melhora o tempo de instalação e o conjunto de trabalho, consulte a coluna Tudo sobre CLR de maio de 2006, em msdn.microsoft.com/magazine/cc163610.

O código do .NET Framework representa uma grande parte dos programas do Silverlight, por isso não ter o NGen disponível no Silverlight 2 e 3 fazia uma grande diferença no tempo de inicialização. O compilador JIT estava demorando muito para otimizar e compilar o código de biblioteca no caminho de inicialização de cada programa.

A nossa solução para esse problema foi fazer com que o compilador JIT não otimizasse a geração de código no Silverlight 2 e 3. Ainda era preciso compilar o código, mas, como o compilador JIT estava produzindo um código simples, a compilação não levou muito tempo. Em comparação com aplicativos de área de trabalho tradicionais, a maioria dos programas escritos para cenários sofisticados na Web para aplicativos de Internet são pequenos e não executam por muito tempo. Ainda mais importante é que normalmente eles são programas interativos, o que significa que passam a maior parte do tempo esperando a entrada do usuário. Nos cenários visados pelo Silverlight 2 e 3, ter um tempo de inicialização rápido era muito mais importante do que gerar código otimizado.

À medida que os aplicativos Web do Silverlight evoluíram, fizemos alterações para que a experiência continue sendo positiva. Por exemplo, adicionamos suporte para instalar e executar aplicativos do Silverlight na área de trabalho no Silverlight 3. Normalmente, esses aplicativos são maiores e trabalham mais do que os aplicativos menores e interativos encontrados no cenário Web clássico. O próprio Silverlight adicionou muita funcionalidade que exige bastantes recursos computacionais, como o suporte para entrada por toque no Windows 7 e a manipulação avançada de fotos que vemos no site do Bing Maps. Todos esses cenários exigem que o código seja otimizado para executar com eficiência.

O Silverlight 4 oferece desempenho na inicialização e código otimizado. Agora o compilador JIT usa as mesmas otimizações no Silverlight que ele utiliza para aplicativos de área de trabalho do .NET. Podemos fazer otimizações porque habilitamos o NGen para os assemblies .NET Framework do Silverlight. Quando você instala o Silverlight, compilamos automaticamente todo o código gerenciado que vem no tempo de execução do Silverlight e o salvamos no seu disco rígido. Quando um usuário executa o programa do Silverlight, ele começa executando sem ter de esperar que o código do Framework seja compilado. Igualmente importante, agora otimizamos o código no seu programa do Silverlight para que ele execute com mais rapidez, e podemos compartilhar o código do Framework entre vários programas do Silverlight em execução no computador do usuário.

O Silverlight 4 cria imagens nativas para os assemblies .NET Framework durante a instalação. Existem alguns aplicativos em que o desempenho na inicialização é o único desempenho que importa. Pense no Bloco de Notas como um exemplo: é importante que ele inicie rapidamente, mas depois que você começa a digitar, não importa a rapidez da execução do Bloco de Notas (desde que ele execute mais rápido do que você digita). Para esta classe de programas, o tempo necessário para o JIT compilar o código de inicialização do aplicativo pode levar a um aumento do desempenho. A maioria dos aplicativos será inicializada de 400 ms a 700 ms mais rápido no Silverlight 4 e terá uma melhora de até 60% no desempenho durante a execução.

A BCL (Biblioteca de Classes Base) está no centro das APIs gerenciadas que agora são suportadas pelo NGen no Silverlight 4. Vejamos o que há de novo na BCL.

Nova funcionalidade da BCL

Muitos nos novos aprimoramentos da BCL no Silverlight 4 também são novos no .NET Framework 4 e já foram abordados nesse contexto. Apresentarei uma rápida visão geral do que incluímos no Silverlight 4.

Os contratos de código são uma maneira interna de expressar precondições, pós-condições e invariáveis de objeto no código do Silverlight. Eles podem ser utilizados para melhor expressar suposições no código e podem ajudar a encontrar bugs logo de início. São muitos os benefícios adicionais relacionados ao uso de contratos de código. Você pode obter mais informações na coluna Tudo sobre CLR de agosto de 2009 escrita por Melitta Andersen, em msdn.microsoft.com/magazine/ee236408, no site Contratos de Código do DevLabs, em msdn.microsoft.com/devlabs/dd491992, e no blog da equipe da BCL, em blogs.msdn.com/bclteam.

Na maioria das vezes, as tuplas são usadas para retornar valores múltiplos de um método. Elas são utilizadas com frequência em linguagens funcionais, como F#, e linguagens dinâmicas, como IronPython, mas são fáceis de usar no Visual Basic e no C#. Você pode obter mais informações sobre o design de tuplas na coluna Tudo sobre CLR de julho de 2009, por Matt Ellis, em msdn.microsoft.com/magazine/dd942829.

Lazy<T> é uma maneira fácil de inicializar objetos lentamente. A inicialização lenta é uma técnica comum que os aplicativos podem usar para adiar o carregamento ou a inicialização de dados até a primeira vez que eles sejam necessários.

Os novos tipos de dados numéricos BigInteger e Complex estão disponíveis no SDK do Silverlight 4 em System.Numerics.dll. BigInteger representa um inteiro de precisão arbitrária e Complex representa um número complexo com componentes reais e imaginários.

Agora Enum, Guid e Version dão suporte a TryParse, assim como muitos dos outros tipos de dados da BCL, o que proporciona uma maneira mais eficiente de criar instâncias a partir de uma cadeia de caracteres que não gera exceções quando ocorrem erros.

Enum.HasFlag é um novo método prático que pode ser usado para verificar facilmente se existe ou não um sinalizador definido em um enum Flags, sem ter de lembrar como usar os operadores bit a bit.

String.IsNullOrWhiteSpace é um método conveniente que verifica se uma cadeia de caracteres é ou não nula, se está ou não vazia ou se contém ou não somente espaços em branco.

Agora as sobrecargas de String.Concat e Join usam um parâmetro IEnumerable<T>. Essas novas sobrecargas para String.Concat e Join permitem a concatenação de qualquer coleção que implemente IEnumerable<T> sem a necessidade de primeiro converter a coleção em uma matriz.

Stream.CopyTo facilita a leitura de um fluxo e a gravação do conteúdo em outro fluxo em uma linha de código.

Além desses novos recursos, também fizemos alguns aprimoramentos no armazenamento isolado e habilitamos aplicativos confiáveis do Silverlight a acessar partes do sistema de arquivos diretamente através de System.IO.

Aprimoramentos no armazenamento isolado

Armazenamento isolado é um sistema de arquivos virtual que os aplicativos do Silverlight podem usar para armazenar dados no cliente. Para saber mais sobre armazenamento isolado no Silverlight, consulte a coluna Tudo sobre CLR de março de 2009, em msdn.microsoft.com/magazine/dd458794.

O aprimoramento mais notável feito no armazenamento isolado do Silverlight 4 é na esfera do desempenho. Desde o lançamento do Silverlight 2, temos recebido muitas opiniões de desenvolvedores no que se refere ao desempenho do armazenamento isolado. No Silverlight 3, fizemos algumas alterações que melhoraram significativamente o desempenho de leitura de dados no armazenamento isolado. No Silverlight 4, demos um passo além e resolvemos os afunilamentos de desempenho que os desenvolvedores experimentavam ao gravar dados no armazenamento isolado. De um modo geral, o desempenho do armazenamento foi bem mais aprimorado no Silverlight 4.

Também soubemos pelos desenvolvedores que não havia uma forma fácil de renomear ou copiar arquivos no armazenamento isolado. Para renomear um arquivo, era preciso ler o arquivo original manualmente, criar um novo arquivo e gravar nele e, depois, excluir o arquivo original. A renomeação de um diretório poderia ser feita de modo semelhante, mas exige ainda mais linhas de código, principalmente quando o diretório desejado contém subdiretórios. Isso funciona, mas exige mais código do que você precisa criar e não é tão eficiente quanto instruir o SO a simplesmente renomear o arquivo ou diretório no disco.

No Silverlight 4, adicionamos novos métodos à classe IsolatedStorageFile, que você pode chamar para executar estas operações com eficiência usando uma única linha de código: CopyFile, MoveFile e MoveDirectory. Também adicionamos novos métodos que fornecem informações adicionais sobre arquivos e diretórios no armazenamento isolado: GetCreationTime, GetLastAccessTime e GetLastWriteTime.

Outra novas API que adicionamos no Silverlight 4 é a IsolatedStorageFile.IsEnabled. Antes, a única forma de determinar se o armazenamento isolado estava habilitado era tentar usá-lo e, depois, armazenar IsolatedStorageException subsequente em cache, o que gera uma exceção se o armazenamento isolado está desabilitado. A nova propriedade estática IsEnabled pode ser usada para determinar com mais facilidade se o armazenamento isolado está ou não habilitado.

Muitos navegadores, como o Internet Explorer, Firefox, Chrome e Safari, agora dão suporte a um modo de navegação particular, em que o histórico de navegação, os cookies e outros dados não são mantidos. O Silverlight 4 respeita as configurações de navegação particular, impedindo aplicativos de acessar o armazenamento isolado e armazenar informações no computador local quando navegador está no modo particular. Nessas circunstâncias, a propriedade IsEnabled retornará falso e qualquer tentativa de usar o armazenamento isolado resultará em IsolatedStorageException, o mesmo comportamento se o recurso tiver sido explicitamente desabilitado pelo usuário.

Acesso ao sistema de arquivos

Os aplicativos do Silverlight são executados em uma área de segurança parcialmente confiável. A área de segurança restringe o acesso ao computador local e impõe várias restrições sobre o aplicativo, impedindo que o código mal-intencionado cause danos. Por exemplo, aplicativos parcialmente confiáveis do Silverlight não podem ter acesso direto ao sistema de arquivos. Se um aplicativo precisa armazenar dados no cliente, a única opção é armazenar dados no armazenamento isolado. O acesso ao sistema de arquivos mais amplo só pode ser feito por meio de OpenFileDialog ou SaveFileDialog.

O Silverlight 3 adicionou a capacidade de instalar e executar aplicativos fora do navegador. Isso possibilita alguns cenários offline interessantes, mas esses aplicativos ainda são executados na mesma área de segurança que os aplicativos em execução dentro do navegador. O Silverlight 4 permite que aplicativos executados fora do navegador se configurem para executar com confiança elevada. Tais aplicativos confiáveis podem burlar algumas das restrições da área de segurança após a instalação. Por exemplo, os aplicativos confiáveis podem acessar arquivos de usuário, utilizar o sistema de rede sem restrições de acesso entre domínios, ignorar os requisitos de consentimento e iniciação de usuário e acessar funcionalidade nativa do SO.

Quando um usuário instala um aplicativo que exige confiança elevada, o prompt de instalação normal é substituído por um aviso que informa que o aplicativo pode acessar dados de usuário e só deve ser instalado se for proveniente de sites confiáveis.

Aplicativos confiáveis podem utilizar as APIs de System.IO para acessar diretamente os seguintes diretórios de usuário no sistema de arquivos: MyDocuments, MyMusic, MyPictures e MyVideos. No momento, não são permitidas operações de arquivos fora desses diretórios e elas resultarão em SecurityException. Nesses diretórios, todas as operações de arquivos são permitidas, inclusive leitura e gravação. Por exemplo, um aplicativo de álbum de fotos confiável pode acessar diretamente todos os arquivos contidos no diretório MyPictures. Um aplicativo de edição de vídeo confiável pode salvar um filme no diretório MyVideos.

É importante não codificar caminhos do sistema de arquivos para esses diretórios nos seus aplicativos, uma vez que os caminhos serão diferentes, dependendo do sistema operacional de base. Os caminhos do sistema de arquivos são completamente diferentes entre o Windows e o Mac OS X, mas também podem ser diferentes entre versões do Windows. Para trabalhar corretamente entre todas as plataformas, System.Environment.GetFolderPath deve ser usado para obter os caminhos desses diretórios do sistema de arquivos. Este código usa Environment.GetFolderPath para obter o caminho do sistema de arquivos para o diretório MyPictures, localiza todos os arquivos existentes em MyPictures (e subdiretórios) que terminam com .jpg usando o método System.Directory.EnumerateFiles e adiciona o caminho de cada arquivo a um ListBox:

if (Application.Current.HasElevatedPermissions) {
  string myPictures = Environment.GetFolderPath(
    Environment.SpecialFolder.MyPictures);
  IEnumerable<string> files = 
    Directory.EnumerateFiles(myPictures, "*.jpg", 
    SearchOption.AllDirectories);
  foreach (string file in files) {
    listBox1.Items.Add(file);
  }
}

Este código mostra como criar um arquivo de texto no diretório MyDocuments do usuário usando um aplicativo confiável:

if (Application.Current.HasElevatedPermissions) {
  string myDocuments = Environment.GetFolderPath(
    Environment.SpecialFolder.MyDocuments);
  string filename = "hello.txt";
  string file = Path.Combine(myDocuments, filename);

  try {
    File.WriteAllText(file, "Hello World!");
  }
  catch {
    MessageBox.Show("An error occurred.");
  }
}

System.IO.Path.Combine é usado para combinar o caminho até MyDocuments com o nome do arquivo, o que inserirá o caractere separador de diretórios apropriado para a plataforma de base entre os dois (o Windows usa \, e o Mac usa /). File.WriteAllText é usado para criar o arquivo (ou sobrescrevê-lo se ele já existe) e escrever o texto “Hello World!” no arquivo.

Melhor desempenho e mais recursos

Como você viu, o novo CLR do Silverlight 4 inclui aprimoramentos no tempo de execução e nas classes base. O novo comportamento do GC, o fato de que agora usamos o NGen para assemblies Framework do Silverlight e os aprimoramentos no desempenho do armazenamento isolado significam que seus aplicativos serão inicializados com mais rapidez e executados melhor no Silverlight 4. Os aperfeiçoamentos na BCL permitem que os aplicativos façam mais com menos código, e os novos recursos, como a capacidade de aplicativos confiáveis acessar o sistema de arquivos, facilitam novos cenários de aplicativos interessantes.

Andrew Pardoe é gerente de programa do CLR na Microsoft. Ele trabalha em muitos aspectos do mecanismo de execução de tempos de execução do Silverlight e de desktop. Para entrar em contato com ele, envie um email para andrew.pardoe@microsoft.com.

Justin Van Patten é gerente de programas da equipe do CLR na Microsoft e trabalha com as bibliotecas de classes base. Você pode entrar em contato com ele através do blog da equipe de BCL, em https://blogs.msdn.com/b/bclteam/.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Surupa Biswas, Vance Morrison e Maoni Stephens