jueves, 19 de junio de 2008

Memoria y Performance

C# es el lenguaje de programación que actualmente se utiliza en el desarrollo de aplicaciones ASP.NET, una de sus principales ventajas es que cuenta con "recolector de basura" o Garbage Collector.
Esto es un proceso que revisa la memoria buscando aquellas asignaciones que los programadores ya no utilizamos.
Por ejemplo: si en alguna parte de la aplicación tenemos algo como

1: public void AlgunMetodo()
2: {
3: AlgunaClase miObjeto = new AlgunaClase();
4: // hacemos lo que tenemos que hacer con el objeto
5: ...
6: }


Cuando la ejecución del metodo finaliza, la memoria utilizada por "miObjeto" pasa a ser inaccesible dado que no tenemos la referencia a esa zona de memoria. En algún momento el Garbage Collector encontrará esa zona de memoria y se dará cuenta que ya no se puede acceder desde la aplicación y la devolvera al sistema de manera que se pueda volver a utilizar.

Antes de la existencia del Garbage Collector, los programadores teníamos que tener mucho cuidado y liberar la memoria antes de perder la referencia correspondiente.

El problema es que el Garbage Collector comienza a funcionar cuando se ha consumido el 90% de la memoria !!!
Esto significa que en ese punto ya estamos hasta las manos, y como si fuera poco se agrega un proceso al pool de ejecución que baja la performance de forma notable.

La solución a esta situación es implementar la interfaz IDisposable.

Esta interfaz obliga a las clases que la implementan a codificar el método Dispose(), en el ejemplo anterior si en la definición de AlgunaClase se implementa la interfaz, podríamos hacer lo siguiente:



1: public void AlgunMetodo()
2: {
3: AlgunaClase miObjeto = new AlgunaClase();
4: // hacemos lo que tenemos que hacer con el objeto
5: miObjeto.Dispose();
6: }


Con eso ya solucionamos el problema, dado que el metodo Dispose() devuelve la memoria ocupada sin tener que llegar al 90%.
El problema que tenemos ahora es que debemos acordarnos de invocar al metodo para todos los objetos que utilizamos, es para volverse locos.

Entonces veamos cómo hay que implementar IDispose para una clase:


1: public class AlgunaClase : IDisposable
2: {
3: // Este es el constructor de la clase
4: public AlgunaClase()
5: {
6: // se crea un arreglo de 100 elementos
7: // hay que considerar que _Datos es una
8: // referencia a un arreglo creado en el
9: // momento en que se crea una instancia
10: // de esta clase
11: _Datos = new OtraClase[100];
12: }
13: private OtraClase[] _Datos;
14:
15: // Este es el destructor o finalize de
16: // la clase, se invoca automáticamente
17: // cuando un objeto es destruido.
18: // Esto ocurre cuando se sale del ámbito
19: // de existencia del objeto.
20: public ~AlgunaClase()
21: {
22: // Invoca a Dispose, la sobrecarga que
23: // utiliza argumento para liberar los
24: // recursos propios de la instancia
25: // en este ejemplo es _Datos pero no
26: // libera la memoria asignada al arreglo
27: Dispose(false);
28: }
29:
30: // Aquí va lo que esta clase necesita
31: ...
32:
33: // Este es el método que debe implmentarse
34: // para cumplir con la interfaz
35: public void Dispose()
36: {
37: // En este caso se invoca el metodo con
38: // un parametro que obliga a liberar los
39: // recursos consumidos internamente en la
40: // instancia.
41: Dispose(true);
42: // Con esto se le avisa al Garbage Collector
43: // que no debe hacer nada con esta instancia
44: GC.SuppressFinalize(this);
45: }
46:
47: // Este campo se utiliza para indicar que una
48: // instancia de esta clase ya ejecutó el Dispose
49: private bool _IsDisposed = false;
50:
51: // Este metodo es el que realmente libera la
52: // memoria del objeto cuando el argumento es
53: // verdadero, se liberan los recursos consumidos
54: // dentro del objeto, como ser otro objeto creado
55: // con el operador new.
56: // cuando el argumento es falso, solamente se
57: // liberan recursos no administrados.
58: protected virtual void Dispose(bool disposing)
59: {
60: if (!_IsDisposed)
61: {
62: if (disposing)
63: {
64: // Aquí se trabaja con los recursos administrados
65: // se debe liberar cada uno de los elementos de la
66: // otra clase, se asume que OtraClase también
67: // implementa un metodo Dispose.
68: if (_Datos != null)
69: {
70: foreach(OtraClase o in _Datos)
71: {
72: o.Dispose();
73: }
74: // la referencia _Datos se libera cuando la
75: // instancia es destruida.
76: }
77: }
78: // Aquí se trabaja con los recursos no administrados
79: // como ser una handler a un archivo abierto.
80: }
81: // Finalmente indicamos que ya se hizo el Dispose.
82: this._IsDisposed = true;
83: }
84: }


Observesé que cuando una instancia de AlgunaClase se destruye no se libera la memoria asignada en el arreglo de objetos de OtraClase, esto se puede solucionar invocando al metodo protegido con un parámetro true.

Lo que ocurre es que el diseñador de clases debe analizar mediante algún diagrama de secuencias cómo se utilizan las instancias. En este caso, el destructor está pensado para liberar solo los campos internos de la clase y no los recursos creados internamente; esto es así porque se piensa utilizar las instancias en un bloque using, como se muestra a continuación:

1: using(AlgunaClase miObjeto = new AlgunaClase())
2: {
3: // Hacer lo que se tenga que hacer
4: }


Cuando se codifica de este modo, el compilador genera el siguiente código:

1: AlgunaClase miObjeto = new AlgunaClase();
2: try
3: {
4: // Hacer lo que se tenga que hacer
5: }
6: finally
7: {
8: if (miObjeto != null)
9: miObjeto.Dispose();
10: }


Con lo que se asegura la liberación de los recursos creados internamente en la instancia dado que el método público Dispose invoca al metodo protegido con el argumento true.

Por supuesto es mucho más cómodo utilizar la sentencia using, dejando que el compilador haga su trabajo.

Finalmente me gustaría comentar que tampoco es cuestión de poner código como este para todas las clases que una aplicación necesita, siempre hace falta un mínimo análisis de que es lo que se va a hacer y cómo se hara.

La información sobre todo estó la encontré en MSDN, incluso pueden ver un ejemplo para clases derivadas.

2 comentarios:

Federico Medrano dijo...

Muy buen post, y muy bien explicado. Algo que tambien puede ser de utilidad es la llamda a "GC.Collect()" sólo o con alguno de sus parámetros para que realice la recolección en ese punto. Saludos.

Julio dijo...

La verdad es que hay que tratar de mantener el equipo (hardware) en un funcionamiento normal.
Utilizar el GC.Collect() implica lanzar un proceso que es bravisimo, dado que comienza a revisar todas las asignaciones de memoria y ver si el "dueño" todavía mantiene una referencia a esa zona. Lo que consume recursos especialmente ciclos del microprocesador.
Cuando se trata de nuestras máquinas y estamos en el 60% de la memoria utilizada, estamos contentos.
Pero para una empresa que tiene un servidor Web, la misma situación signifca que está desperdiciando el 40% de su inversión !!!
Por esa razón es que el Garbage Collector solo comienza a funcionar cuando se llega al 90% de memoria ocupada.
Creo que somos los desarrolladores los que tenemos la responsabilidad de mantener la performance de las aplicaciones y si podemos ayudar a mantener la performance del hardware donde se ejecutan mejor aún.