Desenvolvimento - C#

Generics e a Performance no .NET Framework 2.0

Uma das mais comentadas novidades na nova versão do .NET são os Generics, que permitem a criação de estruturas de dados fortemente tipadas. Conheça um pouco mais.

por Fabio Gouw



Uma das mais comentadas novidades na nova versão do .NET são os Generics, que permitem a criação de estruturas de dados fortemente tipadas. Suas grandes vantagens são o aumento de performance e a qualidade de código. Neste artigo, irei exemplificar uma dessas duas vantagens: a melhoria da performance.

Veremos uma breve descrição dos Generics, seus conceitos e implementação, e um comparativo de performance, utilizando a ferramenta CLR Profiler.

O problema no .NET 1.x

No .NET versão 1.x (1.0 e 1.1), quando trabalhamos com as estruturas de dados presentes no namespace System.Collections, todas as informações armazenadas nelas são tratadas como tipos por referência. Isso significa que toda vez que é inserido um tipo de dado por valor em uma dessas estruturas, por exemplo um inteiro num ArrayList, é executado um boxing (conversão de um tipo por valor para um object, alocando uma instância de object e copiando o valor original para ela). Da mesma forma, quando este valor é lido, um unboxing (conversão de um object para o tipo de dado por valor, efetuando uma consistência na tipagem dessa conversão) é necessário.

ArrayList Lista = new ArrayList();
int i = 1;
Lista.Add(i);

No código, criamos uma estrutura de dados ArrayList, e adicionamos um valor nela. Ao executar este procedimento, o .NET está alocando espaço no heap para armazenar o valor inserido no ArrayList.

Da forma contrária, ao ler esse item do ArrayList em uma variável, estamos primeiro validando se o tipo contido no object é compatível com a variável que estamos atribuindo o retorno, e depois copiando o dado para ela.

int j = (int)Lista[0];


Todo esse procedimento gera um overhead, que degrada a performance dos aplicativos.

Generics!

Os Generics do .NET 2.0 permitem que você declare estruturas com tipos fortes de dados, sem comprometer a performance e a produtividade. Com eles, podemos criar listas, listas ligadas, filas, pilhas, etc.

Para utilizar esses tipos Generics, precisamos importar o namespace System.Collections.Generic.

A utilização dessas estruturas é similar à empregada em classes como ArrayList ou Stack, do namespace System.Collections. A diferença é que não precisamos nos preocupar com conversões, na hora de ler valores.

Outra diferença é que, se tentarmos inserir um tipo de dado diferente do que a estrutura foi declarada, teremos um erro em tempo de compilação.

Abaixo está um exemplo de declaração e utilização de Generics:

Stack<decimal> Pilha = new Stack<decimal>();
Pilha.Push(12.36D);
Pilha.Push(0.0000026D);
Pilha.Push(15623.63D);
decimal Valor = Pilha.Pop();

Comparando a performance

Para demonstrar a diferença de performance obtida na utilização de Generics com estruturas de dados, vamos analisar o seguinte código:

using System;
using System.Collections.Generic;
using System.Collections;
using System.Threading;

namespace PerformanceGenerics
{
class Program
{
// Quantidade de loops a serem executados nos testes
private const int QTD_ITENS = 1000000;

static void Main(string[] args)
{
// Analisa os parâmetros passados para o aplicativo
if (args.Length > 0)
{
if (args[0] == "Generics")
ExecutaProcessoComGenerics();
else if (args[0] == "NoGenerics")
ExecutaProcessoSemGenerics();
else
Console.WriteLine("Parâmetro incorreto!");
}
else
{
// Loop executando os dois tipos de implementações
for (int I = 0; I < 10; I++)
{
ExecutaProcessoSemGenerics();
ExecutaProcessoComGenerics();
}
}
// Aguarda pelo usuário pressionar uma tecla
Console.ReadKey();
}

private static void ExecutaProcessoComGenerics()
{
// Cria uma lista utilizando generics
List<int> Lista = new List<int>();
int ContadorInicio = 0;
int ContadorFim = 0;
ContadorInicio = Environment.TickCount;
int Total = 0;
// Adiciona itens na lista
for(int I = 0; I < QTD_ITENS; I++)
Lista.Add(I);
// Executa uma soma com todos os itens da lista
for (int I = 0; I < QTD_ITENS; I++)
Total += Lista[I];
ContadorFim = Environment.TickCount;
// Exibe quantos milissegundos foram necessários
// para executar o processamento
Console.WriteLine("Tempo de processamento com Generics: "
+ (ContadorFim - ContadorInicio).ToString());
}

private static void ExecutaProcessoSemGenerics()
{
// Cria um arraylist
ArrayList Lista = new ArrayList();
int ContadorInicio = 0;
int ContadorFim = 0;
ContadorInicio = Environment.TickCount;
int Total = 0;
// Adiciona itens na lista
for(int I = 0; I < QTD_ITENS; I++)
Lista.Add(I);
// Executa uma soma com todos os itens da lista.
// Note que um cast sempre será executado
for (int I = 0; I < QTD_ITENS; I++)
Total += (int)Lista[I];
ContadorFim = Environment.TickCount;
// Exibe quantos milissegundos foram necessários
// para executar o processamento
Console.WriteLine("Tempo de processamento sem Generics: "
+ (ContadorFim - ContadorInicio).ToString());
}
}
}


Nesse aplicativo do tipo ConsoleApplication, temos dois métodos que têm a mesma funcionalidade, no entanto possuem implementações diferentes: um utiliza Generics e o outro utiliza um ArrayList. Eles inserem valores em uma estrutura de dados, depois somam todos esses valores. Ao final, exibem a quantidade de milissegundos utilizados nesse processamento.

Executando esse aplicativo sem passar nenhum parâmetro por linha de comando, é feita a execução desses dois métodos 10 vezes. Isto é para comparar o tempo de processamento que cada implementação leva em média.

Pelo resultado do aplicativo, podemos ver que a execução do procedimento utilizando Generics levou em média 18,5% do tempo que é necessário para a mesma implementação utilizando-se ArrayList.

Obs. Lembre-se que os valores apresentados nesse resultado podem variar de acordo com a máquina em que o aplicativo foi executado e seus recursos disponíveis.

Agora vamos utilizar o aplicativo CLR Profiler, que pode ser encontrado no site do MSDN (http://msdn.microsoft.com). Este aplicativo permite a analise da utilização de memória em aplicativos .NET.

Para utilizá-lo na monitoração da nossa aplicação, primeiro devemos configurar qual parâmetro por linha de comando deverá ser passado para ela. Isso pode ser feito no menu File > Set Parameters..., e inserindo o valor “NoGenerics” em “Enter Command Line”. Isso nos ajudará a definir qual implementação da funcionalidade iremos analisar.

Após isso, clique em “Start Application...”, e selecione o executável a ser analisado. Nessa primeira análise, iremos verificar a utilização de memória para uma implementação com ArrayList.

Obs. Utilizando o CLR Profiler, o tempo de execução da aplicação cresce muito. Não se preocupem, isso é normal!

Observem os resultados obtidos. No primeiro gráfico (menu View > Call Graph), vemos que a quantidade de memória utilizada foi de 19 MB. 11 MB utilizado para armazenar os valores por tipo (inteiro), e 8 MB para armazenar os tipos por referência, utilizados durante o boxing.

No segundo gráfico (menu View > Time Line), temos a dimensão do que foi utilizado durante a execução do aplicativo. Em vermelho estão as variáveis por valor (inteiro), e em amarelo os tipos objects que foram utilizados durante o processamento.

Agora vamos analisar o mesmo aplicativo, só que utilizando uma implementação com Generics. Para isso, vamos ajustar o parâmetro de linha de comando para “Generics”, no CLR Profiler.

Quando executamos, temos os seguintes resultados:

Note que foram utilizados 8 MB na execução do programa, a metade do que foi necessário para a execução da implementação com ArrayList.

Veja também que foram utilizados basicamente tipos por valor (inteiros), ou seja, não ocorreu boxing/unboxing nesse procedimento. Além disso, não foi executado casting (conversão), quando utilizamos as informações para a soma.

Conseguimos resultados melhores no ponto de vista de tempo de processamento e utilização de memória, ao trabalharmos com Generics.

Conclusão

Uma das maiores novidades no .NET Framework 2.0 são os Generics, que possibilitam trabalhar com estruturas de dados numa forma mais performática, entre outras vantagens.

Vimos nesse artigo a grande diferença de uso de memória e tempo de execução entre implementações sem Generics e com Generics. Cabe à nós, desenvolvedores, utilizar essa nova característica do .NET de forma correta para que possamos extrair a melhor performance dos nossos aplicativos.

Fabio Gouw

Fabio Gouw - Analista Programador
MCP, MCAD, MCSD .NET