.NET

17 set, 2012

Melhorando o desempenho de aplicativos .NET – Parte 14

Publicidade

No artigo anterior, discutimos a otimização de array em .NET. Hoje, vamos nos concentrar em trabalhar com coleções. É claro que existem dois tipos básicos de coleções: listas e dicionários, onde as listas são baseadas em índices e dicionários são baseados em chaves. As coleções implementam o ICollection, IEnumerable, ou interfaces IList. Se os tipos implementam IDictionary separadamente ou em adição a estas três interfaces, são chamados de dicionários.

Problemas de desempenho com coleções

Nesta seção, vamos apontar os problemas relacionados ao desempenho associadas às coleções.

Problemas de boxing

Se você usa uma coleção como um ArrayList para armazenar tipos de valores como integer ou float, em cada item será realizado o boxing (um tipo de referência é criado e o valor copiado) quando ele for adicionado à coleção. Se você estiver adicionando muitos itens para a coleção, a sobrecarga pode ser excessiva. O problema é ilustrado pelo seguinte trecho de código.

ArrayList al = new ArrayList();

for (int i=0; i

Para evitar esse problema, considere o uso de um array, ou a criação de uma classe de coleção personalizada para o seu tipo de valor específico.

Segurança do thread

As coleções geralmente não são por padrão thread-safe. É seguro para múltiplos threads ler a coleção, mas qualquer modificação para ela produz resultados indefinidos para todos os threads que a acessam. Para fazer uma coleção segura de threads, faça o seguinte:

  • Crie um wrapper thread-safe usando o método sincronizado e acesse a coleção exclusivamente por meio desse wrapper:
// Creates and initializes a new ArrayList.

ArrayList myAr = new ArrayList();   

// add objects to the collection   

// Creates a synchronized wrapper around the ArrayList.

ArrayList mySyncdAr = ArrayList.Synchronized( myAr );

Use a instrução lock em C # (ou SyncLock no Visual Basic. NET) na propriedade SyncRoot ao acessar a coleção:

ArrayList myCollection = new ArrayList();

lock( myCollection.SyncRoot ) {

  // Insert your code here.

}

Você também pode implementar uma versão sincronizada da coleção derivando da coleta e implementando de um método sincronizado usando a propriedade SyncRoot.

Enumeração de overhead

As coleções .NET Framework fornecem um enumerador pelo excesso de processamentos IEnumerable.GetEnumerator, que é menos do que ideal para uma série de razões:

  • O método GetEnumerator é virtual, então a chamada não pode ser embutida;
  • O valor de retorno é uma interface IEnumerator, ao invés de um tipo exato; como resultado, o enumerador exato não pode ser conhecido no tempo de compilação;
  • O método MoveNext e as propriedades atuais são, novamente, virtuais e assim não podem ser embutidas;
  • O IEnumerator.Current requer um tipo de retorno System.Object, ao invés de um tipo de dados mais específicos, que podem exigir boxing e unboxing, dependendo dos tipos de dados armazenados na coleção.

Como resultado, há tanto heap gerenciado e sobrecarga de função virtual associada com foreach nos tipos de coleta simples. Isso pode ser um fator importante no desempenho de regiões sensíveis no seu aplicativo. Falaremos sobre enumeração de overhead adicional para este artigo.

Princípios básicos de coleção

Esta seção resume as diretrizes que ajudam você a usar tipos de coleção .NET Framework de forma mais eficiente e a evitar erros comuns de desempenho:

Analise suas necessidades antes de escolher o tipo de coleção

Tente determinar se você realmente precisa usar uma coleção. Os arrays são geralmente mais eficientes, especialmente se você precisa armazenar tipos de valor. Você pode utilizar os seguintes critérios ao determinar quais coleções são apropriadas:

  • Você precisa classificar sua coleção?
  • Você precisa procurar sua coleção?
  • Você precisa acessar cada elemento pelo índice?
  • Você precisa de uma coleção personalizada?

Você precisa classificar sua coleção?

Se a resposta for sim, faça o seguinte:

  • Utilize ArrayList para vincular os dados somente de leitura classificados para um datagrid como fonte de dados. Isso é melhor do que usar um SortedList se você só precisar ligar dados somente de leitura usando os índices no ArrayList (por exemplo, porque os dados precisam ser exibidos em um datagrid somente de leitura). Os dados são recuperados em um ArrayList e classificados para a exibição;
  • Use SortedList para classificar os dados que forem mais estáticos e precisam ser atualizados com pouca frequência;
  • Use NameValueCollection para ordenar as strings;
  • SortedList pré-ordena os dados durante a construção da coleção. Isto resulta na criação de um processo relativamente dispendioso para a lista classificada, mas todas as alterações aos dados existentes e quaisquer pequenas adições da lista são automática e eficientemente recorridas conforme as alterações são feitas. SortedList é adequado para a maioria dos dados estáticos, com pequenas atualizações.

Você precisa pesquisar a sua coleção?

Se sim, faça o seguinte:

  • Use Hashtable se você precisa pesquisar sua coleção de forma aleatória com base em um par de chaves/ valores;
  • Use StringDictionary para buscas aleatórias em dados de sequência;
  • Use ListDictionary para tamanhos menores que dez.

Você precisa acessar cada elemento pelo índice?

Se precisar, faça o seguinte:

  • Use ArrayList e StringCollection para um acesso de índice baseado em zero para os dados;
  • Use SortedList, Hashtable, ListDictionary e StringDictionary para acessar elementos, especificando o nome da chave.
  • Use NameValueCollection para elementos de acesso, quer através da utilização de um índice baseado em zero ou especificando a chave do elemento.

Lembre-se de que os arrays fazem isso melhor do que qualquer outro tipo de coleção.

Você precisa de uma coleção personalizada?

Considere o desenvolvimento de uma para abordar os seguintes cenários:

  • Desenvolva a sua própria coleção personalizada caso seja necessário empacotar por referência, pois todas as coleções padrãp são passadas por valor. Por exemplo, se a coleção armazena objetos que são relevantes apenas no servidor, você pode querer empacotar a coleção por referência e não por valor;
  • Você precisa criar uma coleção fortemente tipada de seu próprio objeto personalizado para evitar os custos de upcasting ou downcasting – ou ambos. Note que se você criar uma coleção assim, herdando CollectionBase ou Hashtable, você ainda acaba pagando o preço de casting, porque, internamente, os elementos são armazenados como objetos;
  • Você precisa de uma coleção somente de leitura;
  • Você precisa ter a sua própria serialização de comportamento personalizado para sua coleção rigidamente tipada. Por exemplo, se você estender Hashtable e estão armazenando objetos que implementam IDeserializationCallback, é necessário customizar a serialização para o fator para o cálculo dos valores de hash durante o processo de serialização;
  • Você precisa reduzir o custo da enumeração.

Quando puder, inicialize as coleções para o tamanho correto

Faça o mesmo caso saiba exatamente, ou mesmo aproximadamente, o número de itens que você deseja armazenar em sua coleção – a maioria dos tipos de coleções permite que você especifique o tamanho com o construtor, como mostra o exemplo a seguir.

ArrayList ar = new ArrayList (43);

Mesmo que a coleção seja capaz de ser redimensionada dinamicamente, é mais eficiente alocá-la com a capacidade inicial correta ou aproximada (com base em seus testes).

Considere enumerar o overhead

Uma coleção suporta enumeração de seus elementos usando a construção foreach implementando IEnumerable.

Para reduzir a sobrecarga de enumeração em coleções, considere implementar o enumerator padrão da seguinte maneira:

  • Se implementar IEnumerable.GetEnumerator, implemente também um método GetEnumerator Método não-virtual. A sua classe IEnumerable.GetEnumerator deverá chamar esse método Nonvirtual, que , por sua vez, deve retornar uma estrutura de um enumerador  público, como mostra o seguinte exemplo de código.
class MyClass : IEnumerable

{

  // non-virtual implementation for your custom collection

  public MyEnumerator GetEnumerator() {

    return new MyEnumerator(this); // Return nested public struct

  }

  // IEnumerator implementation

  public IEnumerator.GetEnumerator() {

    return GetEnumerator();//call the non-interface method

  }

}

A construção de linguagem foreach chama GetEnumerator Nonvirtual da sua classe, caso ela estipule explicitamente este método. Caso contrário, ela chama o IEnumerable.GetEnumerator se a sua classe herdar do IEnumerable. Chamar o método Nonvirtual é um pouco mais eficiente do que chamar o método virtual através da interface.

  • Implemente a propriedade IEnumerator.Current explicitamente na estrutura do enumerador. A implementação das coleções .NET faz com que a propriedade retorne um System.Object, ao invés de um objeto fortemente tipado – o que incorre em uma sobrecarga de casting. Você pode evitar essa sobrecarga, retornando um objeto fortemente tipado ou o tipo exato de valor ao invés de System.Object em sua propriedade atual. Por você ter implementado explicitamente um método GetEnumerator não-virtual (não o IEnumerable.GetEnumerator), o tempo de execução é capaz de chamar diretamente a propriedade Enumerator.Current, ao invés de chamar a propriedade IEnumerator.Current, obtendo assim os dados desejados diretamente e evitando o vazamento ou sobrecarga ou boxing, eliminando as chamadas de funções virtuais e permitindo o alinhamento.

A implementação deve ser semelhante à seguinte:

// Custom property in your class

//call this property to avoid the boxing or casting overhead

Public MyValueType Current {

  MyValueType obj = new MyValueType();

  // the obj fields are populated here

  return obj;

}

// Explicit member implementation

Object IEnumerator.Current {

get { return Current} // Call the non-interface property to avoid casting

}

Implementar o padrão Enumerator implica ter um tipo público extra (o enumerator) e vários métodos públicos extras que estão lá apenas por razões de infra-estrutura. Estes tipos aumentam a complexidade percebida da API e devem ser documentados, testados, versionados e assim por diante. Como resultado, você deve adotar esse padrão apenas quando o desempenho for fundamental.

O exemplo de código a seguir ilustra o padrão:

public class  ItemTypeCollection: IEnumerable

{

   public struct MyEnumerator : IEnumerator

   {

      public ItemType Current { get { } }

      object IEnumerator.Current { get { return Current; } }

      public bool MoveNext() { }

         }

   public MyEnumerator GetEnumerator() { }

   IEnumerator IEnumerable.GetEnumerator() { }

}

Para tirar proveito do alinhamento JIT, evite usar membros virtuais em sua coleção, a menos que realmente precise de extensibilidade. Além disso, limite o código na propriedade Current de retorno do valor atual para permitir o alinhamento ou, em outra alternativa, utilize um campo.

Prefira implementar IEnumerable com simultaneidade otimista

Existem duas formas legítimas para implementar a interface IEnumerable. Com a abordagem de simultaneidade otimista, você assume que a coleção não será modificada enquanto estiver sendo enumerada. Caso seja, você pode jogar um InvalidOperationException. Uma abordagem pessimista alternativa é tirar uma foto da coleção no enumerator para isolá-lo de mudanças na coleção subjacente. Na maioria dos casos, modelo de simultaneidade otimista oferece um desempenho melhor.

Considere realizar boxing no overhead

Ao armazenar os tipos de valor em uma coleção, você deve considerar o overhead envolvido, porque realizar boxing no overhead pode ser excessivo, dependendo do tamanho da coleção e da taxa de atualização ou acesso de dados. Se você não precisa da funcionalidade fornecida pelas coleções, considere usar arrays para evitar a sobrecarga de boxing.

Considere for ao invés de  foreach

Utilize for ao invés de foreach (C#) para iterar o conteúdo de arrays ou coleções em código crítico de desempenho, principalmente se você não precisa das proteções oferecidas pelo foreach.

Considere Boxing Overhead

Ao armazenar os tipos de valor em uma coleção, você deve considerar o overhead envolvido, porque o overhead Boxing pode ser excessivo, dependendo do tamanho da coleção e da taxa de atualização ou acesso de dados. Se você não precisa da funcionalidade fornecida pelas coleções, considere usar arrays para evitar a sobrecarga de boxing.

Considere Instead of foreach

Utilize Instead of foreach (C #) para iterar o conteúdo de arrays ou coleções em código crítico de desempenho, principalmente se você não precisa das proteções oferecidas pelo foreach.

Tanto o foreach em C #, quanto o For Each em Visual Basic .NET utilizam um enumerator para fornecer navegação melhorada por meio de matrizes e coleções. Como discutido anteriormente, implementações típicas de enumerators, tais como os fornecidos pelo Framework .NET, terão conseguido gerenciar o heap e o excesso de processamentos de função virtual associadas ao seu uso.

Se for possível usar a instrução for para iterar sobre sua coleção, considere fazê-lo no código de performance sensível, para evitar a sobrecarga.
Implemente coleções fortemente tipadas para prevenir o excesso de processamento de casting.

Implemente coleções fortemente tipadas para evitar o overhead

Faça isso tendo seus métodos aceitos ou retorne os tipos específicos ao invés do tipo de objeto genérico. StringCollection e StringDictionary são exemplos das coleções fortemente tipadas para strings.

Seja eficiente com os dados nas coleções

Ao lidar com um grande número de objetos, torna-se muito importante gerenciar o tamanho de cada um. Por exemplo, faz pouca diferença se você usar um (Int16) int curto, Integer (Int32), ou (Int64) longo para uma única variável, mas pode fazer uma enorme diferença se você tiver um milhão deles em uma coleção ou em uma array. Se você estiver lidando com tipos primitivos ou complexos de objetos definidos pelo usuário, certifique-se de não alocar mais memória do que precisa, caso esteja criando um grande número desses objetos.

Tipos de coleção

Vamos resumir as principais questões que você deve considerar ao usar os tipos diferentes de coleções.

ArrayList

A classe ArrayList representa uma lista que redimensiona dinamicamente, conforme novos itens forem adicionados à lista e sua capacidade atual for ultrapassada. Considere as seguintes recomendações ao usar uma:

  • Utilize uma ArrayList para armazenar tipos de objetos personalizados e, particularmente, quando os dados mudam com frequência e você frequentimente adicona, ou excluir operações;
  • Utilize TrimToSize depois de atingir o tamanho desejado (e não houver mais inserções esperadas) para cortar a ArrayList para um tamanho exato. Isto também otimiza o uso de memória. No entanto, esteja ciente de que se o seu programa precisar inserir novos elementos posteriormente, o processo de inserção agora será mais lento, pois a ArrayList deve agora crescer dinamicamente;
  • Armazene os dados pré-classificados e utilize o ArrayList.BinarySearch para pesquisas eficientes. As classificações e pesquisas lineares usando Contains são dispendiosas. Isso é, essencialmente, para uma classificação de dados feita somente uma vez, mas se for necessário realizar a triagem frequente, um SortedList pode ser mais benéfico, pois ela reclassifica automaticamente a coleção inteira após cada inserção ou atualização;
  • Evite ArrayList para armazenamento de strings. Use uma StringCollection ao invés.

Hashtable

O Hashtable representa uma coleção de pares de chave/ valor que são organizados com base no código hash da chave. Considere as seguintes recomendações ao usar Hashtable:

  • Hashtable é adequado para grande número de registros e dados que podem ou não serem alterados com frequência. Mudanças frequentes nos dados tem uma sobrecarga extra de cálculo do valor de hash em comparação aos dados que não são alteradas frequentemente.
  • Utilize Hashtable para dados consultados com frequência, por exemplo, catálogos de produtos onde o ID do produto é a chave.

HybridDictionary

O HybridDictionary é implementado internamente, utilizando uma ListDictionary quando a coleção for pequena ou um Hashtable quando a coleção aumentar de tamanho. Considere as seguintes recomendações:

  • Utilize HybridDictionary para armazenamento de dados quando o número de registos for baixo na maior parte do tempo, com aumentos ocasionais de tamanho. Se você tiver certeza de que o tamanho da coleção será sempre elevado ou sempre baixo, você deve escolher entre o Hashtable e ListDictionary respectivamente. Isso evita o custo extra do HybridDictionary, que atua como um wrapper em torno de ambas as coleções;
  • Utilize HybridDictionary para dados consultados com frequência;
  • Não use HybridDictionary para classificar os dados. Ele não é otimizado para classificação.

ListDictionary

Utilize ListDictionary para armazenar pequenas quantidades de dados (menos de dez itens). Isso implementa a interface IDictionary usando uma implementação de lista linkada por uma ligação. Por exemplo, uma classe factory que implementa o padrão factory pode armazenar objetos instanciados em um cache utilizando uma ListDictionary, para que eles possam ser servidos diretamente do cache na próxima vez em que um pedido de criação for feito.

NameValueCollection

Isso representa uma coleção ordenada de chaves string associadas e valores de strings que podem ser acessados com a chave ou com o índice. Por exemplo, você pode usar um NameValueCollection se precisar exibir assuntos registrados por alunos de uma turma especial porque ele pode armazenar os dados em ordem alfabética dos nomes dos alunos.

  • Utilize NameValueCollection para armazenar strings de pares de chave/ valor em uma ordem pré-classificada. Note que você também pode ter várias entradas com a mesma chave.
  • Utilize NameValueCollection para mudar frequentemente dados onde você precisa inserir e excluir itens regularmente.
  • Utilize NameValueCollection quando precisar armazenar itens e para recuperação rápida.

Queue

Queue representa uma coleção de objeto FIFO (“First-in, First-out”, ou então “Primeiro a entrar, Primeiro a sair”). Considere as seguintes recomendações para o uso do Queue:

  • Utilize Queue quando você precisar acessar os dados em sequência, com base na prioridade. Por exemplo, um aplicativo que varre a lista de espera de pedidos de reserva de avião e dá prioridade alocando lugares vagos para os passageiros no início da fila;
  • Utilize Queue quando precisar processar os itens sequencialmente de uma maneira FIFO (“first-in, first-out”);
  • Se você precisar acessar os itens com base em um identificador de string, use um NameValueCollection no lugar.

SortedList

A SortedList representa uma coleção de pares de chave/ valor que são classificados pelas chaves e são acessíveis por chave e por índice. Novos itens são adicionados na ordem de classificação e as posições dos itens existentes são ajustadas para acomodar os novos itens. Os custos da criação associados a uma SortedList são relativamente altos, então você deve usá-lo nas seguintes situações:

  • A coleção pode ser usada onde os dados forem mais estáticos e apenas alguns registros precisarem ser adicionados ou atualizados ao longo de um período de tempo, p -r exemplo, um cache de informações de funcionários. Isso pode ser atualizado pela adição de uma nova chave com base no número de funcionários, que será adicionado rapidamente na SortedList, enquanto uma ArrayList precisa executar o algoritmo de ordenação mais uma vez para que a alteração delta seja mais rápida em SortedList;
  • Use SortedList para recuperação rápida de objeto, usando um índice ou uma chave. Ele é bem adequado para as circunstâncias em que você precisa para recuperar um conjunto de objetos classificados, ou para consultar um objeto específico;
  • Evite o uso de SortedList para alterações de dados grandes, pois o custo da inserção de grande quantidade de dados é alto. Em vez disso, prefira uma ArrayList e classifique-a chamando o método de classificação. O ArrayList usa o algoritmo QuickSort por padrão. O tempo gasto pelo ArrayList é muito menor para a criação e a classificação do que o tempo necessário para o SortedList;
  • Evite usar SortedList para armazenar strings por causa da sobrecarga de qualidade. Use um StringCollection ao invés.

Stack

Isso representa uma simples coleção de objetos LIFO (“Last in, Last Out”, ou “Último a entrar, Primeiro a sair”). Considere as seguintes recomendações para o uso um Stack:

  • Use Stack em cenários em que for necessário processar os itens de uma maneira LIFO. Por exemplo, um aplicativo que precisa monitorar os dez usuários visitantes mais recentes de um site durante um período de tempo;
  • Especifique a capacidade inicial, se você souber o tamanho;
  • Use Stack onde for possível descartar os itens depois de processá-lo;
  • Use Stack onde não for necessário acessar itens arbitrários na coleção.

StringCollection

Isso representa um conjunto de strings e é uma ArrayList fortemente tipada. Considere as seguintes recomendações para a utilização de StringCollection:

  • Use StringCollection para armazenar dados de string que mudam com frequência e precisam ser recuperados em grandes partes;
  • Use StringCollection para ligação de string data a uma rede de dados. Isto evita os custos de downcasting a uma string durante a recuperação;
  • Não use StringCollection para strings de classificação ou para armazenar dados pré-classificados.

StringDictionary

Este é um Hashtable com a tecla fortemente tipada como uma string, ao invés de um objeto. Considere as seguintes recomendações para a utilização de StringDictionary:

  • Use StringDictionary quando os dados não mudarem com frequência porque a estrutura subjacente é um Hashtable usado para armazenar strings fortemente tipadas;
  • Use StringDictionary para armazenar strings estáticas que precisam ser frequentemente consultadas;
  • Prefira sempre StringDictionary sobre Hashtable para armazenar cadeia de pares de chave/ valor se quiser preservar o tipo de string para garantir a segurança do tipo.

***

Artigo original disponível em: http://blog.monitis.com/index.php/2012/05/21/improving-net-application-performance-part-14-collections/