Apr 25, 2006

Notas de desempeño

A pesar de todos los comentarios puristas, usualmente absurdos y sin fundamentos, referentes a la plataforma Mono (y MS .NET), escribir código en esa plataforma realmente agiliza el desarrollo, en .NET no todo es "arrastrar y soltar", ni llenar las "formas" y cambiarles los colores arbitrariamente, ni hacer supuesta orientación a objetos copiando trozos de código (estáticos por cierto) a diferentes métodos. Escribir código en .NET cualquiera lo puede hacer, pero el hacerlo bien requiere habilidades y experiencia en otros lenguajes.

Podríamos pensar para escribir una aplicación de tiempo real debemos seleccionar un lenguaje que pueda al final general un binario dependiente de plataforma y arquitectura, de esta forma se podrán explotar, entre otras cosas, las capacidades de CPU y un manejo adecuado de memoria y administración de esta, ¿Qué es lo malo?, hay que saber como hacerlo y aprender lleva tiempo y tiempo es dinero, más tiempo significa más inversión y eso es algo de lo que usualmente no disponemos, además la plataforma que se ofrece a través de la conjunción del CLR y el CIL hacen que podamos tener lo mejor de los dos mundos, hacer que el tiempo de desarrollo se reduzca y el desempeño sea al menos semejante a una aplicación de arquitectura y plataforma dependiente, es obvio que un desempeño excelente en comparación a un compilado-dependiente no se podrá obtener, pero sin embargo siempre se busca el mejor.

Mi proyecto actual del trabajo es el claro ejemplo del extremo, donde se debe escribir una aplicación en .NET con un desempeño excelente, el retardo máximo de actualización es de 3 segundos, actualmente lo he bajado de 6-8 a 3-1 segundos, cosa que me agrada, pero seguro se puede mejorar más, algunas cosas que me ayudaron a mejorar el desempeño fue:

  1. Parar el uso de Enumeradores, reemplazalos con punteros, con código no administrado.
  2. No usar indexadores para acceder a arreglos cambiando el código a un uso de aritmética de punteros, con código no administrado.
  3. Concatenaciones a travéz de System.Text.StringBuilder, en vez de utilizar el clásico "cadena + cadena".
  4. Débido a que recibo información binaria de estructuras escritas en C mediante un broadcast, es necesario generar las versiones C# correspondientes a aquellas struct en C con un Marshalling de modo que se pueda hacer un casting de tipo *(Estructura *) ptr.

Existe un artículo que muestra un buen caso de ejemplo indicando desempeño al momento de hacer ciclos a arreglos, dentro se utiliza un ejemplo de punteros escritos en C++ administrado, la versión de C# sería algo así:

using System;

namespace Iterations
{
public class Pointer
{
public unsafe static void iterate (Data data)
{
double d;
fixed (double *ptr = &data.Array [0]) {
int l = data.Array.Length;
for (int i = 0; i < l; i++)
d = *(ptr +i);
}
}
}
}

Los resultados son contundentes:

repetitions: 1000
iterations: 1.000000e+006

Enumeration: 32.87 seconds
Indexing: 11.246 seconds
Indirect Arrays: 10.172 seconds
Direct Arrays: 5.44 seconds
Pointer Math: 4.828 seconds

El retardo principal se debe a los objetos generados durante la enumeración, foreach no es la elección en aplicaciones de alto desempeño, sin duda es sencillo de implementar pero es lento al ejecutar, la solución más rápida a travéz de punteros se debe a que validaciones como el índice del arreglo no es considerado, dejando todo la lógica de validación al programador.

Hay cosas que me faltan de eliminar como ese abuso exagerado de boxing/unboxing al momento de tener mi colección de estructuras recibidas por el broadcast, además de la creación innecesario de tipos por valor, reemplazando con una lista enlazada en código no administrado y pasos por referencia respectivamente. Un ejemplo de esto sería:

using System;

namespace Research
{
unsafe public struct MyStruct
{
public MyStruct (int integer)
{
Integer = integer;
Next = null;
}

public int Integer;
public MyStruct *Next;

public override string ToString ()
{
return "Integer: "+Integer+" at "+
"Ptr "+((int)&(*Next));
}
}

//Quick sample, don't bother me ;-)
public class Sample
{
unsafe public static void Main (string []args)
{
MyStruct obj1 = new MyStruct ();
MyStruct obj2 = new MyStruct ();
obj1.Next = null;
obj2.Next = &obj1;
obj1.Integer = 1;
obj2.Integer = 2;
ChangeByPointer (&obj1.Integer);
ChangeByReference (ref obj2.Integer);
Console.WriteLine (obj1 +" * "+ obj2);
}

unsafe public static void ChangeByPointer (int *reference)
{
*(reference) += 5;
}

unsafe public static void ChangeByReference (ref int reference)
{
reference += 5;
}
}
}

Algo interesante es el hecho de que ambos métodos generan la misma secuencia de instrucciones CIL, lo cual obviamente indica que son iguales, por tal razón cualquier elección es buena:

//... more before

// method line 5
.method public static hidebysig
default void ChangeByPointer (int32* reference) cil managed
{
// Method begins at RVA 0x21c4
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: dup
IL_0002: ldind.i4
IL_0003: ldc.i4.5
IL_0004: add
IL_0005: stind.i4
IL_0006: ret
} // end of method Sample::default void ChangeByPointer (int32* reference)

// method line 6
.method public static hidebysig
default void ChangeByReference (int32& reference) cil managed
{
// Method begins at RVA 0x21cc
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: dup
IL_0002: ldind.i4
IL_0003: ldc.i4.5
IL_0004: add
IL_0005: stind.i4
IL_0006: ret
} // end of method Sample::default void ChangeByReference (int32& reference)
//more later...

Faltan detalles por mejorar, pero sin duda "jugar" con punteros siempre será lo mejor.