viernes, 19 de enero de 2007

Eficiencia y eficacia en el código

O bien, ¿Qué es más rápido (y eficiente): Te llamo por tu nombre, o busco de qué familia eres?

Tengo un antiguo proyecto que comencé en .NET 1.1 y que tenía abandonado. Ahora que ya sé más cosas de la plataforma, quiero retomarlo y forrarme como un cerdo cuando lo venda. Pero lo voy haciendo con calma y mirando con cariño el código.

Una de las cosas que he mirado es la creación y gestión de ventanas MDI. Yo provengo del mundillo de Delphi y seguramente haga muchas cosas en .NET como las haría o hacía en Delphi (igual de bien o igual de mal xD). El caso es que para la gestión de los MDIChild, una de las formas de controlar que no creas más de una instancia de un form determinado, es recorrerte los forms abiertos comprobando a qué clase pertenecen sus instancias. En .NET se puede hacer así, o se puede hacer comprobando el nombre que se le dió a la instancia cuando se creó (lo cual se me antoja mucho más sencillo y más "lógico").

Veamos dos fragmentos de código:


... código anterior...

// Verificaremos si está creado el formulario.
bool vExist = false;
int i = 0;
// Recorremos la lista de MDIs a ver si alguno es el que buscamos.
while ((i < this.MdiChildren.Length) & (!vExist)) {
if ((Form)this.MdiChildren[i] is AfrmRejillaClientes) {
vExist = true;
((AfrmRejillaClientes)this.MdiChildren[i]).WindowState = FormWindowState.Normal;
}
i++;
}
// Si NO lo hemos encontrado...
if (!vExist) {
AfrmRejillaClientes frmRejClientes = new AfrmRejillaClientes();
frmRejClientes.MdiParent = this;
frmRejClientes.Show();
}

... código posterior...


Como se puede ver, estamos utilizando el comprobador 'is' para verificar que el tipo del formulario que estamos comprobando (nótese el cast al tipo Form sobre el objeto 'this') sea el que hemos indicado. La otra forma de hacerlo es comprobando el nombre de la instancia:


... código anterior...

// Verificaremos si está creado el formulario.
bool vExist = false;
int i = 0;
// Recorremos la lista de MDIs a ver si alguno es el que buscamos.
while ((i < this.MdiChildren.Length) & (!vExist)) {
if (((Form)this.MdiChildren[i]).Name == "AfrmRejillaClientes") {
vExist = true;
((AfrmRejillaClientes)this.MdiChildren[i]).WindowState = FormWindowState.Normal;
}
i++;
}
// Si NO lo hemos encontrado...
if (!vExist) {
AfrmRejillaClientes frmRejClientes = new AfrmRejillaClientes();
frmRejClientes.MdiParent = this;
frmRejClientes.Show();
}

... código posterior...


Una cosa curiosa es que yo pensaba que había que utilizar el método '.ToString()' y no la propiedad '.Name'. Pero bueno la cuestión es otra: ¿Cuál de las dos formas de escribir este código es más eficiente? Mi primer pensamiento fue que la más eficiente es la que comprueba el nombre. ¿Será cierto? Veamos pues...

MSIL correspondiente al primer código:

IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: br.s IL_002e
IL_0006: ldarg.0
IL_0007: call instance class [System.Windows.Forms]System.Windows.Forms.Form[] [System.Windows.Forms]System.Windows.Forms.Form::get_MdiChildren()
IL_000c: ldloc.1
IL_000d: ldelem.ref
IL_000e: isinst ProgramaGestion.Forms.AfrmRejillaClientes
IL_0013: brfalse.s IL_002a


MSIL correspondiente al segundo código:

IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: br.s IL_0038
IL_0006: ldarg.0
IL_0007: call instance class [System.Windows.Forms]System.Windows.Forms.Form[] [System.Windows.Forms]System.Windows.Forms.Form::get_MdiChildren()
IL_000c: ldloc.1
IL_000d: ldelem.ref
IL_000e: callvirt instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Name()
IL_0013: ldstr "AfrmRejillaClientes"
IL_0018: call bool [mscorlib]System.String::op_Equality(string,
string)
IL_001d: brfalse.s IL_0034

Como se puede apreciar, evidentemente me equivocaba. La segunda opción es mucho menos eficiente que la primera. La llamada a la función 'isinst' del MSIL ocupa 4 bytes; y todo el tinglado que se monta en el segundo listado, para hacer una comparación de cadenas, es mucho más pesado (15 bytes). No me he puesto a mirar cuantos ciclos de reloj supondrían, pero seguro que gana en eficiencia y eficacia la comprobación del Tipo. De calle, además.

Que sí, que en estos tiempos de PCs con Gigabytes de memoria, Gigahertzios de frecuencia y cientos de Gigabytes de capacidad de almacenamiento, ahorrarse 15 miserables bytes puede parecer una soberana chorrada, pero mira de 15 en 15 bytes te puedes evitar que un programa te ocupe 100 Megas en memoria al ejecutarse (habéis mirado alguna vez en el administrador de tareas, el proceso 'devenv.exe'? Yo me horrorizo casi siempre). Y además es sana costumbre de buen programador el optimizar tu código hasta donde te sea posible... ¿no?

6 comentarios:

Rox dijo...

Interesante, yo que sigo en Delphi y trasteo en C# lo hago con la comprobacion del caption del formulario mediante una funcion que devuelve true o false en caso de encontrar o no encontrar la mencionada ventana, teniendo presente que no toco los caption de los forms para nada en mi aplicacion por supuesto. Para C# pensaba hacerlo igual pues hasta ahora no he tenido problemas, pero probaremos esta otra forma que propones, porque como muy bien dices siempre es bueno optimizar nuestro código todo lo que podamos.

Ian Marteens dijo...

Le voy a echar un vistazo a las dos variantes que mencionas. Ten en cuenta que el IL es muy engañoso, porque después pasa Mr. JIT y la que monta es de cuidado.

Para quien no conozca el truco: la "mejor" forma de ver el código real que se genera estando todas las optimizaciones imaginables activas:

1- Primero, tocar la configuración Release de Visual Studio, para que incluya genere un fichero PDB de depuración con la configuración Release. Eso se hace normalmente sólo con la configuración Debug, pero al activar esto, se permite poder iniciar una sesión de depuración con la versión "buena" o "definitiva" del programa compilado.

2- Asegurarse de que Release está optimizando el código.

3- Los dos puntos anteriores se cambian en las propiedades de cada proyecto. Este otro se cambia en la configuración "global" de Visual Studio: en Proyectos y Soluciones, hay que buscar una opción que anula la optimización JIT durante la depuración. Inicialmente viene o venía activa: hay que apagarla para que el JIT optimice el código nativo.

Luego sólo hace falta depurar la versión Release, y cuando llegues al punto que deseas, activar la ventana Disassembler (Ctrl+Alt+D).

Ian Marteens dijo...

... y sí, la primera es más eficiente. No contando los bytecodes, pero piensa en lo que hace falta en cada caso: con la versión "is", sólo hay que comparar punteros a los descriptores de tipos (probablemente el isinst se implemente mediante una llamada a GetType que ahora queda oculta en el código IL), y en el otro caso, hay que comparar las representaciones de cadenas. Si las cadenas comparten prefijo, como DialogXXX y DialogYYY, tienes que analizar todo el prefijo común antes de detectar la diferencia (a no ser que las cadenas estén "internalizadas", pero esa es otra historia).

Yo incluso probaría esto otro:

obj.GetType() == typeof(LoQueSea)

si se trata de búsqueda exacta por tipo. Creo que va a dar un IL más extenso, pero el código nativo final puede ser más eficiente, al no tener que ver si, una vez que falla la igualdad, queda la posibilidad de que estemos buscando un ancestro.

Anónimo dijo...

Mmmm...

¿Por qué no pones un break para que salga del bucle y te ahorras lo que quede de él? ¿No estabas hablando de optimizar tamaño de código? Y el gasto de tiempo, ¿qué?

Anónimo dijo...

Soy el del "mmmm..."

¿Por qué en lugar de tener una variable para comprobar si la ficha se ha encontrado no usas la variable i?

Es decir, con un break dentro del código, si i vale el límte del bucle más 1 es que no se ha encontrado la ficha.

No sé yo hasta que punto vale la pena enrollarse en lo que te enrrollas para luego dejar pasar esas optimizaciones que tu haces mejor que un compilador y dejar al compilador que haga las que él sabe hacer mejor.

Almirante Coronel RAMMS dijo...

Yo le puse asi, y jala de lo lindo...
bool existe = false;
foreach (Form item in MdiChildren)
{
if (item is InventarioProductos)
{
item.BringToFront();
existe = true;
break;
}
}
if (!existe)
{
InventarioProductos nuevo = new InventarioProductos();
nuevo.MdiParent = this;
nuevo.Show();
}