Comprobación del Rendimiento de los Genéricos en C# 2.0

Por Felipe Arcos Vélez

Descarga Descargar Ejemplo de este artículo.

Contenido

 1. Introducción
 2. Genéricos
 3. No olvidarse de IEquatable<T>...
 4. ...Y menos, de IComparable<T>
 5. Confrontación: Rendimiento Genéricos vs. No Genéricos
 6. Conclusión

1. Introducción

Este artículo expone las diferencias del rendimiento entre los genéricos y el código tradicional; se incluyen gráficas comparativas y se tratan algunas recomendaciones a tener en cuenta para obtener un buen rendimiento.

 

2. Genéricos

Los genéricos era lo que le faltaba a los lenguajes de .NET, tales como C# y VB .NET, para que los desarrolladores tuviéramos menos dolores de cabeza y podamos:

  • Utilizar estructuras de datos con un buen desempeño, sin tener que escribir nuestras propias estructuras optimizadas para un tipo de dato en particular; es decir, que nuestro empleador nos pague por código optimizado y realmente no tengamos que hacer ese trabajo; que el .NET Framework lo haga por nosotros.

  • Evitar que nos estén llamando a media noche o nos hagan trasnochar y trabajar fines de semana porque el código está generando errores por el manejo de tipos erróneos; es decir, porque en un descuido metimos peras donde se esperaban manzanas y el compilador no tenía el mecanismo adecuado para indicarnos acerca de esta inconsistencia. Técnicamente hablando, metimos un cadena donde se esperaba un entero.

  • Ahorrar nuestro preciado tiempo evitando tratar de descifrar el tipo de información que una colección maneja; es decir, que los demás escriban código legible y fácil de mantener para todos (a quién no le ha pasado que, haciéndole mantenimiento al código de otro, se ha encontrado con sorpresas tales como que un desarrollador, por no crear una clase con atributos apropiados, ha preferido manejar una colección donde en cada posición maneja un tipo de dato diferente simulando los atributos de la clase y, a la hora de recuperar los valores, uno debe recordar las posiciones y con qué tipo de conversión se debe recuperar el valor).

Los genéricos sencillamente son una extensión del lenguaje que nos permite parametrizar los tipos de datos que el código maneja, a nivel de:

  • Clases

  • Estructuras

  • Interfaces

  • Métodos

De ahora en adelante, cuando utilicemos genéricos, podremos indicarlo diciendo: clase genérica, estructura genérica, método genérico, interfaz genérica y colección genérica.

En resumen, los beneficios que ofrecen los genéricos son los siguientes:

  • Permiten especializar el código en función del tipo de dato que se requiere.

  • Evitan la utilización de conversiones de tipo (cast), optimizando el rendimiento del código debido a que no se realizan las verificaciones en tiempo de ejecución que se requieren para realizar las conversiones.

  • Optimizan la manipulación de variables del tipo valor en colecciones, debido a que se requieren menos operaciones de boxing y unboxing.

  • Ayudan a generar código menos propenso a errores debido a que las verificaciones de tipo se hacen en tiempo de compilación en vez de emplear tiempo de ejecución para ello.

Los genéricos se definen utilizando paréntesis angulares, con la posibilidad de definir más de un parámetro de tipo; por ejemplo, en el siguiente código se definen 2 parámetros de tipo identificados como KeyType y ElementType:

public class Dictionary<KeyType, ElementType> {
       public void Add(KeyType key, ElementType val) {...}
       public ElementType this[KeyType key] {...}
}

Nótese que en el siguiente código se le indica al compilador que KeyType va a ser un string y que el parámetro de tipo ElementType va a ser de tipo Customer:

Dictionary<string, Customer> dict = 
       new Dictionary<string, Customer>();
dict.Add("Peter", new Customer());
Customer c = dict["Peter"];

 

3. No olvidarse de IEquatable<T>...

IEquatable<T> es para los genéricos lo que la mantequilla es para el pan: nacieron el uno para el otro. IEquatable<T> es una interfaz que contiene un único método llamado Equals, similar al de la clase Object, con la diferencia de que el argumento es tipado, el cual es más eficiente que el de la clase Object en casos tales como:

  • Cuando se efectúan Operaciones de Contains en colecciones, debido a que se evitan las conversiones.

  • Cuando se realizan comparaciones con elementos del tipo valor, debido a que se evita la implementación por defecto del método Equals de la clase Object, el cual a nivel de variables de tipo valor, utiliza reflexión para determinar la igualdad.

El siguiente ejemplo muestra una estructura implementando la interfaz IEquatable<T>:

struct CircleIEquatable : IEquatable<CircleIEquatable> {
    private int radio;
       
    public CircleIEquatable(int radio){
       this.radio = radio;
    }
       
    public bool Equals(CircleIEquatable b){
       return this.radio == b.radio;
    }
}

Observa que el parámetro del método Equals es del mismo tipo de la clase, evitando así realizar conversiones para realizar la compararación.

 

4. ...Y menos, de IComparable<T>

La situación con IComparable<T> es similar al caso anterior; esta interfaz define el método CompareTo que también es tipado. En el siguiente ejemplo se muestra cómo la clase CircleIEquatable implementa la interfaz genérica IComparable<T>:

struct CircleIEquatable : IEquatable<CircleIEquatable>, IComparable<CircleIEquatable>{
   ...
   public int CompareTo(
      CircleIEquatable bCircle){
      if (this.radio > bCircle.radio)
         return 1;
      if (this.radio < bCircle.radio)
         return -1;
      return 0;
   }
}

Con esta implementación se logran beneficios en rendimiento; por ejemplo, cuando se utilizan los métodos Sort y BinarySearch en colecciones, ya que estos métodos constantemente deben invocar al método CompareTo de los elementos de la colección.

 

5. Confrontación: Rendimiento Genéricos vs. No Genéricos

  1. Test Primitivos

    Esta prueba consiste en comparar el rendimiento de 2 clases que utilizan una colección para guardar valores enteros. La clase TestNoGenericsPrimitivos es la versión que utiliza una ArrayList para almacenar los enteros sin emplear genéricos, y la clase TestGenericsPrimitivos es la version que sí utiliza genéricos para realizar la misma tarea; observa que en la versión que no se utilizan genéricos se deben realizar operaciones de boxing y unboxing para almacenar y recuperar los elementos, mientras que en la otra versión estas operaciones no son requeridas.

    public class TestNoGenericsPrimitivos : TestBase { 
    

    private IList l;       public override void Init() {         l = new ArrayList();         for (int i = 0; i < sizeTest; i++){             l.Add(i); //boxing         }     }       public override void Test() {         int size = l.Count;         for (int i = 0; i < size; i++) {             int value=(int)l[i]; //unboxing         }     }       public override void Release() {         l.Clear();         l = null;     } }       public class TestGenericsPrimitivos : TestBase {     private IList<int> lGeneric;       public override void Init() {         lGeneric = new List<int>();         for (int i = 0; i < sizeTest; i++){             lGeneric.Add(i);         }     }       public override void Test() {         int size = lGeneric.Count;         for (int i = 0; i < size; i++) {             int value = lGeneric[i];         }     }       public override void Release() {         lGeneric.Clear();         lGeneric = null;     }   }

Como se muestra en el diagrama de la Figura 1, se obtiene un rendimiento muy superior gracias a los genéricos, debido a que se evitan las operaciones de boxing y unboxing tradicionales:

![Bb972288.art301-img01-494x251(es-es,MSDN.10).gif](images/Bb972288.art301-img01-494x251(es-es,MSDN.10).gif "Bb972288.art301-img01-494x251(es-es,MSDN.10).gif")  
**Figura 1:** Resultados rendimiento ArrayList vs. List\<int\> Generic. Volver al texto.  
  

La gráfica habla por sí sola; el rendimiento de los genéricos es superior, básicamente debido a lo siguiente:

  - No se realizan operaciones de boxing cuando se agregan los valores a la colección.

  - No se realizan operaciones de unboxing cuando se recuperan los valores de la colección.
  1. Test Objetos

    El objetivo de esta prueba es observar si existe algún incremento del rendimiento utilizando colecciones genéricas que almacenen elementos de tipo Object.

    public class TestNoGenericsRerencia : TestBase { 
    

    private IList l;       public override void Init() {         l = new ArrayList();         for (int i = 0; i < sizeTest; i++){             l.Add(new object());         }     }       public override void Test(){         int size = l.Count;         for (int i = 0; i < size; i++){             object value = l[i];         }     }       public override void Release(){         l.Clear();         l = null;     } }       public class TestGenericsReferencia : TestBase {     private IList<object> l;       public override void Init() {         l = new List<object>();         for (int i = 0; i < sizeTest; i++){             l.Add(new object());         }     }       public override void Test(){         int size = l.Count;         for (int i = 0; i < size; i++){             object value = l[i];         }     }       public override void Release(){         l.Clear();         l = null;     } }

![Bb972288.art301-img02-494x251(es-es,MSDN.10).gif](images/Bb972288.art301-img02-494x251(es-es,MSDN.10).gif "Bb972288.art301-img02-494x251(es-es,MSDN.10).gif")  
**Figura 2:** Resultados rendimiento ArrayList vs. List\<Object\> Generic.  
  

En este caso la diferencia en rendimientos es nula, y debería ser así ya que como todo lo que se esta manipulando es de tipo Object, no se requieren conversiones ni verificaciones de tipo en tiempo de ejecución.
  1. Test No Objects

    El objetivo de esta prueba es verificar si existe un incremento en el rendimiento cuando se utilizan colecciones genéricas que almacenen variables del tipo referencia que no sean instancias directas de la clase Object; en este caso, se utilizó la clase StringBuilder para realizar la prueba. Igualmente, se puede elegir cualquier otra clase.

    public class TestNoGenericsReferenciaNoObject: TestBase{ 
    

    private IList l;       public override void Init() {         l = new ArrayList();         for (int i = 0; i < sizeTest; i++){             l.Add(new StringBuilder());         }     }       public override void Test() {         int size = l.Count;         for (int i = 0; i < size; i++) {             StringBuilder value = (StringBuilder)l[i];         }     }       public override void Release() {         l.Clear();         l = null;     }   }       public class TestGenericsReferenciaNoObject : TestBase {     private IList<StringBuilder> l;       public override void Init() {         l = new List<StringBuilder>();         for (int i = 0; i < sizeTest; i++){             l.Add(new StringBuilder());         }     }       public override void Test() {         int size = l.Count;         for (int i = 0; i < size; i++) {             StringBuilder value = l[i];         }     }       public override void Release() {         l.Clear();         l = null;     }   }

![Bb972288.art301-img03-494x251(es-es,MSDN.10).gif](images/Bb972288.art301-img03-494x251(es-es,MSDN.10).gif "Bb972288.art301-img03-494x251(es-es,MSDN.10).gif")  
**Figura 3:** Resultado rendimiento ArrayList vs. List\<StringBuilder\>.  
  

Se presenta un mejor rendimiento con la colección genérica, aunque no es tan marcada la diferencia como en la primera prueba, donde los elementos eran del tipo primitivo, y esto se debe a que las conversiones de tipo entre variables del tipo referencia es mucho menos costosa que las operaciones de boxing y unboxing requeridas con las variables de tipo valor, sin embargo, la ganancia en rendimiento de la colección genérica es apreciable.
  1. IEquatable<T>

    Esta prueba consiste en observar el incremento en el rendimiento del método Contains de las colecciones, cuando los elementos que ésta contiene implementan la interfaz IEquatable<T>.

    struct Circle : IComparable{
    

    private int radio;            public Circle(int radio){        this.radio = radio;     }       public override bool Equals(object o) {         if (o is Circle) {             Circle b = (Circle) o;             return this.radio == b.radio;         }         return false;     } }   public class TestNoIEquatable : TestBase {     private IList<Circle> l;       public override void Init() {         l = new List<Circle>();         for (int i = 0; i < sizeTest; i++){             l.Add(new Circle(i));         }     }       public override void Test(){         int size = l.Count;         for (int i = 0; i < size; i++) {             l.Contains(new Circle(i));         }     }       public override void Release(){         l.Clear();         l = null;     } }       struct CircleIEquatable : IEquatable<CircleIEquatable> {     private int radio;            public CircleIEquatable(int radio){        this.radio = radio;     }            public bool Equals(CircleIEquatable b){        return this.radio == b.radio;     } }   public class TestIEquatable : TestBase {     private IList<CircleIEquatable> l;            public override void Init(){        l = new List<CircleIEquatable>();        for (int i = 0; i < sizeTest; i++){            l.Add(new CircleIEquatable(i));        }                }            public override void Test(){        int size = l.Count;        for (int i = 0; i < size; i++){            l.Contains(new CircleIEquatable(i));        }     }       public override void Release(){         l.Clear();         l = null;     }   }

![Bb972288.art301-img04-494x251(es-es,MSDN.10).gif](images/Bb972288.art301-img04-494x251(es-es,MSDN.10).gif "Bb972288.art301-img04-494x251(es-es,MSDN.10).gif")  
**Figura 4:** Resultado rendimiento implementando Equatable\<T\>.  
  

Nuevamente el resultado obtenido es alentador a favor de los genéricos, debido principalmente a que en el método Equals ya no se realizan conversiones.

IComparable<T>

Esta prueba evalúa el rendimiento del método Sort cuando se ordenan elementos que implementan la interfaz IComparable<T>.

struct Circle : IComparable{
    ...
    public int CompareTo(object b){
       Circle bCircle = (Circle) b;
       if (this.radio > bCircle.radio) 
return 1;
       if (this.radio < bCircle.radio) 
return -1;
       return 0;
    }
}
 
public class TestNoIComparableGeneric : TestBase { 
    private List<Circle> l;
 
    public override void Init() {
        l = new List<Circle>();
        for (int i = 0; i < sizeTest; i++){
            l.Add(new Circle(i));
        }
    }
 
    public override void Test() {
        l.Sort();
    }
 
    public override void Release() {
        l.Clear();
        l = null;
    }
 
}
 
 
 
struct CircleIEquatable : 
IEquatable<CircleIEquatable>, IComparable<CircleIEquatable>{
    ...
    public int CompareTo(
CircleIEquatable bCircle){
if (this.radio > bCircle.radio) 
return 1;
if (this.radio < bCircle.radio) 
return -1;
return 0;
    }
}
 
public class TestIComparableGeneric : TestBase { 
    private List<CircleIEquatable> l;
 
    public override void Init() {
        l = new List<CircleIEquatable>();
        for (int i = 0; i < sizeTest; i++){
    l.Add(new CircleIEquatable(i));
        }
    }
 
    public override void Test(){
        l.Sort();
    }
 
    public override void Release(){
        l.Clear();
        l = null;
    }
 
}

Bb972288.art301-img05-494x251(es-es,MSDN.10).gif
Figura 5: Resultado rendimiento implementando IComparable<T>.

Nuevamente la diferencia del rendimiento es bastante significativa, debido a que no se realizaron conversiones ni verificaciones en tiempo de ejecución en la versión con genéricos.

 

6. Conclusión

Las conclusiones, por prioridad, deben ser las siguientes:

  1. Debo empezar a utilizar genéricos, si aún no lo he hecho.

  2. Debo recordar implementar la interfaz IEquatable<T> en todas las clases que defina; principalmente en aquellas que vayan a ser utilizadas en colecciones; es decir, casi todas.

  3. Debo recordar también implementar la interfaz IComparable<T>.

  4. Preferiblemente, debería volverse un hábito utilizar genéricos e implementar siempre las interfaces IEquatable<T> e IComparable<T>.

Bb972288.felipe_arcos_velez(es-es,MSDN.10).gif

Felipe Arcos Vélez se desempeña como Trainer, Consultor y Arquitecto de Software, y lleva más de 5 años trabajando con las tecnologías de desarrollo Microsoft. Es Ingeniero Mecatrónico, especialista en Telemática, y cuenta con las certificaciones MCT, MCSD .NET, MCAD .NET y MCP. Ha colaborado como instructor de la versión 2003 del programa Desarrollador 5 Estrellas y es además docente universitario.