O Visual Studio é um ambiente de desenvolvimento maravilhoso em que o IntelliSense®, a depuração integrada, a ajuda online e fragmentos de código melhoram o desempenho para os desenvolvedores. Mas só o fato de estar escrevendo código rapidamente, não significa estar escrevendo código rápido.

Durante os últimos meses, o CLR performance team (equipe de desempenho CLR) se reuniu com vários clientes para analisar questões de desempenho em algumas das suas aplicações. Um problema que ocorria freqüentemente, estava relacionado ao tempo de inicio de aplicações cliente. Nesta coluna, apresentarei as lições que aprendemos analisando estas aplicações.

Planejando o desempenho

O sucesso para atingir as metas de desempenho, depende do processo que estivermos usando. Um processo bom pode ajudar a atingir o nível de desempenho necessário. As seguintes quatro simples regras ajudarão:

Pense em termos de Cenários

Os cenários podem ajudar a focalizar no que realmente é importante. Por exemplo, se estivermos projetando um componente que será usado ao iniciar, é provável que o mesmo seja chamado somente uma vez (quando a aplicação inicia). De um ponto de vista de desempenho, desejamos minimizar o uso de recursos externos; tais como rede ou disco; pois é provável que venham a ser um gargalo. Se não considerarmos esta questão, poderemos gastar tempo aperfeiçoando o código, sem atingirmos nenhuma melhoria significativa. Descobrimos que a maioria do tempo de inicio será gasto no carregamento de DLLs ou lendo arquivos de configuração.

Em cenários de inicialização, deveríamos analisar quantos módulos estão carregados e como a aplicação acessará os dados de configuração (os arquivos em disco, o registro e outros). Fazer o Refactor do código removendo algumas dependências ou a carga de módulos demorados (o que abordaremos depois) poderia resultar em grande melhoria de desempenho.

Para o código que é chamado repetidamente (como hash ou funções de análise gramatical), a velocidade é fundamental. Para aperfeiçoar estes códigos, precisamos focar nos algoritmos e minimizar o custo por instrução. A localização dos dados também é importante. Por exemplo, se o algoritmo acesa grandes trechos de memória, é provável que as faltas do cache L2, impeçam os algoritmos de rodar na velocidade mais rápida possível. Duas métricas que podem ser usadas neste cenário são: o custo de CPU por repetição e as alocações por repetição. O ideal é que ambos sejam baixos. Estes exemplos deveriam provar que o desempenho é muito dependente do contexto e a análise dos cenários pode ajudá-lo a descobrir variáveis importantes.

Da próxima vez, antes de começar a escrever código, dedique algum tempo para analisar os cenários em que o código rodará, e identifique quais são as métricas e quais os fatores que impactarão no desempenho. Se aplicar estas simples recomendações, o código executará bem por projeto.

Determine as Metas

Este é um conceito trivial, mas às vezes as pessoas esquecem que, para determinar se uma aplicação é rápida ou lenta, é preciso estabelecer metas contra as quais medir. Todas as metas de desempenho definidas (por exemplo, que a janela principal da aplicação deverá ser pintada completamente em três segundos após o início da aplicação), deverão estar baseadas no que pensamos que seja a expectativa do cliente. Às vezes, não é fácil pensar em termos de números complicados, logo no início do ciclo de desenvolvimento do produto (quando se supõe que foram estabelecidas as metas de desempenho), mas é melhor estabelecer uma meta e ter que revisá-la depois, do que não ter meta nenhuma.

Torne Iterativo o Ajuste Fino de Desempenho

Este processo consiste em medir, investigar e refinar/corrigir. Desde o início até o fim do ciclo de vida do produto, precisamos medir o desempenho da aplicação em um ambiente seguro e estável. Deveríamos evitar a variabilidade relativa a fatores externos (por exemplo, deveríamos desabilitar antivírus ou qualquer procedimento de atualização automática; tal como o SMS; para que não interfiram com a execução do teste de desempenho). Uma vez medido o desempenho da aplicação, precisamos identificar as mudanças que resultarão nas maiores melhorias. Com base nesta análise, mudar o código e começar o ciclo novamente.

Conheça Bem sua Plataforma

Antes de começar a escrever o código, deveríamos conhecer o custo de cada característica que usaremos. Por exemplo, deveríamos saber que Reflection geralmente é cara, portanto precisamos ter cuidado quando a usamos (não significa que a reflexão deveria ser evitada, apenas que tem exigências de desempenho específicas).

Agora, avancemos mais um passo além da fase de planejamento e tentemos resolver alguns problemas de codificação. O tempo de iniciação pode ser um problema para aplicações cliente com UI complexas e com conexões a fontes de dados múltiplas. O usuário final espera que a janela principal apareça assim que fizer duplo clique no ícone da aplicação, por isso, o tempo de iniciação causa um impacto tão grande com relação ao modo como os clientes encaram a aplicação. O conhecimento dos dois tipos de cenários de iniciação com os quais iremos lidar; iniciação a “frio” e a “quente”, irá nos auxiliar a focalizar os nossos esforços.

Temos um exemplo de iniciação a frio quando a aplicação reinicia após um reboot. Outro exemplo seria quando começamos uma aplicação, a fechamos e a reiniciamos após um longo período de tempo. A iniciação a frio acontece devido a faltas de hardware (hard faults). Quando uma aplicação inicia, caso as páginas requeridas (código, dados estáticos, registro e demais) não estejam presentes na lista de espera do gerenciador de memória do SO, será necessário o acesso a disco para carregar aquelas páginas na memória. Estas solicitações ou faltas de página são conhecidas como faltas de software (soft faults).

No cenário de iniciação a quente (por exemplo, quando uma aplicação gerenciada já rodou uma vez), é provável que a maioria das páginas para os componentes do common language runtime (CLR), já estejam carregadas em memória e disponíveis para o uso do SO, economizando o custoso tempo de acesso a disco. È por este motivo, que uma aplicação gerenciada inicia muito mais rapidamente pela segunda vez. Estas faltas de software são características da iniciação a quente.

Agora que sabemos o que é a iniciação a quente, vejamos como podemos melhorá-la. As seguintes seções abordam medidas concretas que podem ser adotadas.

Carregue Menos Módulos na Iniciação

A iniciação a frio pode se beneficiar da carga de menor quantidade de módulos (como poderíamos imaginar, o resultado é menos acesso a disco). Mesmo a iniciação a quente se beneficiará pela carga de menos módulos, pois será evitado o custo indireto de CPU associado. Para descobrir quais módulos a aplicação carrega, podemos usar o VAdump, o qual vem incluso na Plataforma SDK. Se digitarmos vadump –sop , veremos algo parecido com a Listagem 1.

Listagem 1. VAdump

            Category                        Total        Private Shareable    Shared

            Pages    KBs         KBs       KBs       KBs

Page table pages        33       132       132         0         0

Other system            12        48        48         0         0

Code/StaticData       1739      6956       552       500      5904

Heap                   113       452       452         0         0

Stack                    9        36        36         0         0

Teb                      4        16        16         0         0

Mapped data             98       392         0        28       364

Other data             105       420       416         4         0



Total modules         1739      6956       552       500      5904

Total dynamic data     329      1316       920        32       364

Total system            45       180       180         0         0

Grand Total working set     2113      8452      1652       532      6268



Module Working Set Contributions in pages

Total   Private Shareable    Shared Module

4         2         2         0 HelloWorld.exe

55         3         0        52 ntdll.dll

21         3         0        18 mscoree.dll

16         1         0        15 ADVAPI32.dll

51         3         1        47 KERNEL32.dll

9         1         0         8 RPCRT4.dll

29         2         0        27 SHLWAPI.dll

20         2         0        18 GDI32.dll

36         2         0        34 USER32.dll

39         4         0        35 msvcrt.dll

278        17         3       258 mscorwks.dll

54         8         0        46 MSVCR80.dll

270        14        17       239 mscorlib.ni.dll

30         4         0        26 ole32.dll

21         2         0        19 uxtheme.dll

53         3         0        50 MSCTF.dll

36         8         0        28 shell32.dll

39         2         0        37 comctl32.dll

14         3         0        11 comctl32.dll

59         1         0        58 mscorjit.dll

70         7         2        61 System.ni.dll

34         4         1        29 System.Drawing.ni.dll

245        17        12       216 System.Windows.Forms.ni.dll

98        12        86         0 System.Configuration.ni.dll

52         4         1        47 System.Xml.ni.dll

94         7         0        87 gdiplus.dll

12         2         0        10 OLEAUT32.DLL
        

Existem alguns módulos CLR que devem ser carregados todas as vezes, mas podemos ter alguma flexibilidade com outros. No exemplo anterior, se não estivermos usando o XML e o System.Xml.ni.dll constar da lista de módulos carregados, significa que há pelo menos um módulo na aplicação que faz referência ao System.Xml.dll. Podemos analisar o código para verificar se a referência é realmente necessária. Se removermos as referências desnecessárias, poderemos melhorar o perfil de iniciação e o ambiente de trabalho da aplicação.

Reduzir o número de módulos carregados também pode evitar caminhos de código (code paths) que requeiram módulos adicionais. Com freqüência, a remanufatura do código secundário pode evitar o carregamento de DLLs adicionais. Por exemplo, consideremos o seguinte código:


            void Start() {

                try {
            
                    LaunchApplication();
            
                } catch (Exception e) {
            
                    TypeInAnotherAssembly.DisplayMessageBox("error");
            
                }   
            
            }
            

Quando o CLR just-in-time (JIT) compila o método Start, este precisa carregar todos os assemblys referenciados pelo mesmo. Isto quer dizer que serão carregados todos os assemblys referenciais no manipulador de exceção, embora estes possam não ser necessários a maior parte do tempo em que a aplicação será executada. Em tais situações, o código no manipulador de exceção pode ser movido para um método separado, digamos ProcesException(Exception). Presumindo que o ProcesException é grande o bastante para não se tornar inlined (caso contrário o Microsoft® intermediate language - ou o MSIL code - será bem parecido com o código gerado no exemplo anterior), o tipo do outro assembly não foi referenciado no método Start, portanto o CLR JIT não precisará carregá-lo (se for pequeno o bastante para ser inlined, poderíamos usar um MethodImplAttribute no método, para prevenir o inlining). Só será carregado quando o código levantar uma exceção e o ProcesException terá que ser compilado.

É claro que este é só um exemplo simples; em outras situações teremos que fazer mudanças mais significativas para atingir os mesmos resultados.

Outro modo de reduzir o número de módulos carregados consiste em fundir (merge) vários módulos em um. Evidentemente isto só se aplica se tivermos controle sobre os mesmos. Em termos de CPU, cargas de assembly têm ligação por fusão e sobrecarga de CLR de assembly além das chamadas à LoadLibrary, portanto menos módulos significam menos tempo de CPU. Em termos de uso de memória, menos assemblys também significa que o CLR terá menos estados para manter.

Evite Inicializações Desnecessárias

Pode parecer óbvio, mas evitar inicializações desnecessárias também podem melhorar o tempo de iniciação, e é fácil de entender isto erradamente. No Microsoft .NET Framework, qualquer inicialização que precisar acontecer para uma classe, será executada no construtor da classe. Se este código faz referência para outras classes, pode causar um efeito de cascateamento, em que um número grande de construtores de classe será executado. A Listagem 2 mostra um exemplo simples.

Listagem 2. Construtores de classe

            class DataType

            {
            
                //Create facet checkers
            
                static FacetsChecker stringFacetsChecker =
            
                    new StringFacetsChecker();
            
                static FacetsChecker miscFacetsChecker = new MiscFacetsChecker();
            
                ...
            
            }
            
             
            
            class StringFacetsChecker : FacetsChecker
            
            {
            
                static Regex languagePattern = new Regex(
            
                    "^([a-zA-Z]{1,8})(-[a-zA-Z0-9]{1,8})*$", RegexOptions.None);
            
                ...
            
            }
            

A classe DatatType inicializa dois campos, entre outros. Ativa o construtor de classe para as classes referenciadas. Um exemplo é o da classe StringFacetChecker, que cria uma instância da classe Regex em seu construtor. Mesmo se a instância de Regex for raramente usada, ainda incorrerá no custo da inicialização. Fazer a inicialização de Regex por demanda, ao invés de fazer parte do construtor da classe, poderá reduzir o custo de desempenho da classe DataType para a maioria das aplicações que o utilizam.

Coloque Assemblys Fortemente Nomeados no GAC

Se um assembly não foi instalado no Global Assembly Cache (GAC), incorrerá no custo de verificação de hash de assemblys fortemente nomeados, junto com a validação de imagem do native code generation (NGEN), caso alguma imagem nativa para aquele assembly estiver disponível na máquina. Em outras palavras, se um assembly for fortemente nomeado, o CLR garantirá a integridade do binário do assembly, verificando que o hash criptográfico do assembly case com o manifesto (manifest - lista de carga) do mesmo. Mas se o assembly estiver no GAC, esta verificação pode ser pulada, pois será executado como parte da instalação no GAC, e qualquer atualização requer permissões administrativas. De esta forma, o CLR está basicamente garantido quanto a alterações.

O processo de verificação de hash é caro porque envolve o acesso a todas as páginas do assembly, o que pode ser ruim para a iniciação a frio. Também, a computação do hash é CPU intensiva e, portanto, impactará também a iniciação a quente. A extensão do impacto depende do tamanho do assembly que está sendo verificado.

Se um assembly foi precompilado usando o NGEN, mas não foi instalado no GAC, então durante a vinculação, para realizar a fusão, será necessário verificar se a imagem nativa e o assembly MSIL têm a mesma versão (para evitar situações em que uma versão mais nova do assembly foi distribuída na máquina, mas uma versão mais nova da imagem nativa não foi gerada). Para realizar isto, o CLR precisa acessar páginas do assembly MSIL, o que pode prejudicar o tempo de iniciação a frio.

Fazendo um aparte, se for distribuir assemblys marcados com o AllowPartiallyTrustedCallersAttribute para o GAC, esteja certo de tê-los revisado cuidadosamente, para garantir que não estão vulneráveis a alguma falha de segurança. Assemblys instalados no GAC podem ser chamados por qualquer aplicação de código gerenciado, incluindo código potencialmente perigoso carregado a partir de locais não seguros.

Use o NGEN

O compilador JIT compila métodos na medida em que são requisitados durante a execução. Esta compilação em tempo de execução tem vários efeitos no desempenho. Em primeiro lugar, a compilação JIT consome ciclos de CPU. Em segundo lugar, o código compilado reside em heaps dinamicamente alocados, que são private para cada processo. Poderíamos ter um grande impacto em cenários do tipo Terminal Server, onde a escalabilidade da aplicação pode se beneficiar do compartilhamento de páginas entre sessões do usuário. Finalmente, são acessadas muitas páginas de metadados dentro de assemblys referenciais, uma operação que poderia não ser necessária.

O consumo de CPU durante a compilação JIT pode se tornar um gargalo em inicializações a quente, e os acessos a disco adicionais também podem ter um impacto significativo em cenários de iniciação a frio.

A ferramenta NGEN (instalada com .NET Framework) é usado para precompilar todos os métodos em um único assembly e instala um arquivo imagem nativo na máquina, para que possa ser usado em lugar do código compilado do JIT.

O uso do NGEN pode melhorar a iniciação, pois nenhum recurso de CPU será consumido pelo compilador JIT, menos páginas são acessadas (pois o CLR não precisa procurar metadados em assemblys referenciais) e o compartilhamento de página pelos processos é aumentado porque o código e dados residem em páginas de imagem NGEN (um grande número de páginas de imagem NGEN é somente leitura e, portanto, estas páginas podem ser compartilhadas entre processos)

Note aqui a sutileza da coisa: usar o NGEN significa trocar consumo de CPU por mais acesso a disco, pois a imagem nativa gerada pelo NGEN é provavelmente maior que a imagem MSIL. Poderíamos supor que isto irá prejudicar a iniciação a frio, como resultado do aumento da atividade em disco. Porém, o interessante é que a equipe do CLR Performance observou que se a compilação JIT for completamente eliminada, o tempo de iniciação a frio geralmente diminui. Isto se deve ao fato do CLR carregar bem menos páginas de assemblys referenciais como antes mencionado, e não carregar o mscorjit.dll e arquivos assembly MSIL.

De qualquer maneira, deveríamos sempre medir o tempo de iniciação a frio, para determinar o impacto do NGEN no cenário, e decidir a melhor escolha.

Evite o “Rebasing”

Se usarmos o NGEN, deveremos prestar atenção quanto a ocorrências de rebasing, quando as imagens nativas estiverem carregadas em memória. Se um DLL não consegue carregar em seu endereço básico determinado (porque aquela faixa de endereço já foi alocada para outro módulo ou alocação), o carregador do SO o carregará onde quer que possa ser encaixado. Esta pode ser uma operação muito cara, porque o carregador tem que atualizar todos os endereços de referência para locais dentro do DLL, baseado no novo endereço onde o DLL estava carregado. De um ponto de vista de desempenho é ruim, porque o carregador do SO tem que ler toda página que contém um endereço, e uma vez que uma página for escrita, torna-se private para aquele processo: a página precisa agora ser suportada pelo arquivo de paginação.

Além disto, o tempo de iniciação a frio será impactado porque haverá um custo de CPU associado com a atualização dos endereços do DLL, e haverá mais acessos a disco, pois mais páginas serão acessadas. Se construirmos mais de um DLL como parte da aplicação, os rebasing acontecerão com certeza quando a aplicação for carregada, pois o endereço básico padrão designado para toda DLL, é sempre o mesmo (0x400000).

O modo mais rápido de descobrir se um ou mais módulos foram rebased é usar o VAdump (vadump –sop ) e verificar se há módulos para todas as páginas private. Neste caso, o módulo poderá ter sido rebased para um endereço diferente e suas páginas não poderão ser compartilhadas. Podemos também usar o tlist.exe (a Platform SDK de linha de comando, equivalente ao gerenciador de tarefa) e fazer a verificação cruzada para determinar se os módulos foram carregados nos seus endereços preferenciais.

O tlist listará todos os módulos carregados pelo processo, junto com o endereço onde estão carregados. Poderemos descobrir qual é o endereço básico preferencial, usando a ferramenta Ildasm (instalada com o Visual Studio® 2005 sob o diretório SDK) para analisar a imagem MSIL. Se fizermos um duplo clique no manifesto do assembly, veremos, junto com outras informações, a saída produzida mostrada na Listagem 3.

Listagem 3. Base Address

            .module System.dll

            // MVID: {62EF48F3-0E13-4BDC-A6F6-FCD87801E067}
            
            .custom instance void [mscorlib]System.Security.UnverifiableCodeAttribute::.ctor() =
            
                ( 01 00 00 00 )
            
            .imagebase 0x7a440000
            
            .file alignment 0x00001000
            
            .stackreserve 0x00100000
            
            .subsystem 0x0003       // WINDOWS_CUI
            
            .corflags 0x00000009    //  ILONLY
            
            // Image base: 0x037B0000
        

A ferramenta NGEN usa a propriedade imagebase no manifesto do assembly para configurar o endereço básico para a imagem nativa. Podemos também usar Link –dump –headers , para descobrir o endereço básico preferencial. Arquivos de imagem nativos podem ser achados sob %SystemRoom%\assembly\NativeImages_<.NET Framework version>.

Se descobrirmos que um ou mais módulos foram rebased, poderemos resolver o problema recompilando o código e especificando um endereço base diferente com a opção /baseaddres. No Visual Studio, podemos configurar a opção de endereço básico na aba advanced das propriedades do projeto, clicando no botão Advanced.

Também, podemos usar a ferramenta Rebase - instalada com a plataforma SDK - que não requer recompilação. Imagens nativas são normalmente maiores que o correspondente arquivo MSIL, portanto tenha certeza de levar isto em conta, quando configurar os endereços básicos preferenciais, para evitar conflitos pelo fato de que imagens nativas são maiores. A ferramenta Rebase não funciona com assemblys fortemente assinados, pois invalidaria a assinatura, e o assembly não seria considerado válido.

Configuração da aplicação

A configuração da aplicação também afeta o desempenho da iniciação. O .NET Framework provê apoio para recuperar configurações de aplicações armazenadas em formato XML. Apesar de ser uma funcionalidade conveniente, precisamos prestar atenção quanto ao custo de desempenho. Se a aplicação tem requisitos de configuração simples e tem metas de tempo de iniciação rígidas, entradas de registro ou um arquivo INI simples poderiam ser uma alternativa melhor.

A tabela da Tabela 1 compara dois cenários: usando um arquivo config baseado em XML ou um arquivo texto simples para ler algumas configurações para uma aplicação cliente simples.

Total Working Set (KB) Number of DLLs
Text File 4120 18
Config File 6880 24

Como podemos observar, usar arquivos config tem um impacto tanto nos working sets quanto com o número de DLLs que são carregadas em tempo de iniciação. Existem, é claro, cenários onde arquivos config XML fazem sentido - por exemplo para salvar configurações complexas (opções de debugging/tracing, configuração para bibliotecas diferentes usadas na aplicação e outras) - mas representam um custo desnecessário se tivermos necessidade de configurações muito simples. Por exemplo, usar um arquivo XML para salvar apenas o local da janela principal da aplicação não é uma decisão sábia.

O Impacto de AppDomains

Normalmente, principalmente por razões de segurança, não podemos evitar o uso de AppDomains múltiplos. Porém, isto pode limitar o desempenho da iniciação. Podemos reduzir o impacto de App Domains múltiplas, levando em conta as seguintes recomendações:

Carregue Assemblys como Domain Neutral

Se um assembly estiver carregado como domain neutral, significa que o código pode ser reutilizado em outro AppDomain. Se o assembly está carregado em mais de um AppDomain com vinculação a domínio (que é o padrão), cada AppDomain obtém sua própria cópia do código. Isto implica em vários aspectos de desempenho ruins. Primeiro temos o custo de CPU. Se houver uma imagem nativa para o assembly, só o primeiro AppDomain poderá usar a imagem nativa. Qualquer outro AppDomain terá que compilar o código via JIT, o que pode resultar em um custo de CPU significativo.

Assim, o código compilado via JIT residirá em memória private, não podendo ser compartilhado com outros processos ou AppDomains. Se o assembly tem uma imagem NGEN, então o primeiro AppDomain usa a imagem. Todos os outros AppDomains têm que compilar o código via JIT, o que significa que o DLL MSIL para aquele assembly também estará carregado. Este é o pior cenário possível de uma perspectiva de iniciação a frio, pois o acesso a disco para aquele assembly irá dobrar.

Carregando o assembly como domínio neutro garante que a imagem nativa, se existir alguma, será utilizado em todos os AppDomains criados pala aplicação. Se uma imagem nativa não existir, ainda assim haverá um benefício por carregar os assemblys como domínio neutro, pois o código será compilado apenas uma vez e será compartilhado por todos os AppDomains da aplicação.

Reforce a Comunicação Eficiente entre AppDomains

Não é necessário dizer: quanto menos chamadas entre AppDomains, melhor será de um ponto de vista de desempenho. Chamadas sem argumentos ou com argumentos de tipo primitivos simples resultam em melhor desempenho.

Para métodos com argumentos do tipo referencial, quanto mais complexo o gráfico de objeto mais pobre será o desempenho. No .NET Framework 2.0, a maioria das chamadas entre AppDomain, foram melhoradas para executar mais eficientemente do que no .NET Framework 1.1. Mas ainda há casos em que chamadas de método podem não tirar proveito do desempenho superior. Alguns exemplos comuns são as chamadas a interfaces de assemblys vinculadas a domínio, chamadas com argumentos "ref" ou "out", chamadas a AppDomains com shadow-copying de assemblys ativadas e AppDomains com confiabilidade parcial.

Use o NeutralResourcesLanguageAttribute

Quando um recurso é solicitado, o ResourceManager verifica antes a existência de assemblys satélites para a cultura atual da IU, a seguir para o pai da cultura atual da IU e finalmente para a cultura neutra. Se a cultura atual da IU também for a cultura neutra, então o CLR pode evitar dois lookups de assembly satélite, acessando diretamente os recursos de cultura neutros. O NeutralResourcesLanguageAttribute, permite aos desenvolvedores comunicar ao ResourceManager qual é a cultura neutra. Se o ResourceManager descobrir que a cultura atual da UI é a mesma da cultura neutra para aquele assembly, acessará diretamente os recursos de cultura neutros. Isto evitara lookups de assembly malsucedidos, que tendem a ser mais caros em termos de uso de CPU.

Use a Serialização com Inteligência

Usar a serialização (ou a deserialização) na iniciação pode ter um impacto significativamente negativo de desempenho. Estas são operações inerentemente caras em termos da CPU e de alocação de memória. Além disto, será carregado muito código que provavelmente não seria necessário.

Se tivermos que usar serialização, é melhor usarmos a classe BinaryFormatter em vez da classe XmlSerializer. O BinaryFormatter é implementado na Base Class Library (BCL), ou mscorlib.dll. O XmlSerializer é implementado em System.Xml.dll, o que em alguns cenários pode representar a carga de uma DLL adicional. O BinaryFormatter também tende a ser mais rápido do que o XmlSerializer. Mas como dissemos, estas são diretrizes gerais que precisam ser testadas para cada cenário. A abordagem indicada é a de medir a velocidade e o consumo de memória das duas opções de serialização e então decidir qual executa melhor no cenário específico.

Se tivermos que usar o XmlSerializer, poderemos atingir um desempenho melhor se pre-gerarmos o assembly de serialização, uma nova opção para o .NET Framework 2.0. O XmlSerializer trabalha gerando um assembly para realizar a serialização. Este assembly pode ser gerada instantaneamente ou pode ser pre-gerado usando a ferramenta sgen (sgen é instalado com o .NET Framework 2.0 SDK). Além do trabalho de serialização, a geração instantânea envolve o uso do CodeDOM para gerar o código C#. Isto significa a invocação do compilador C# para compilar o código MSIL, usando reflexão para carregar o assembly e finalmente compilando o código via JIT do assembly. Não será preciso lembrar quão caras estas operações podem ser.