Go to content Go to navigation Go to search

Optimización de Código – Diferencia entre usar Convert y usar cast – C#

June 13th, 2010 by JuanK

Esta es una duda frecuente cuando quieres convertir de un tipo numérico a otro.

En ese caso hay diferencias importantes sobre todo en cuanto a velocidad de procesamiento.

Veamos 2 ejemplos.

 

1 – CONVERTIR DE TIPO DECIMAL (DECIMAL) A TIPO INT (INT32):

Si utilizas el operador de cast, es decir : (int)

Explicación básica:

El runtime lo que hará es simplemente tomar la parte entera del número descartando completamente la parte decimal.

Explicación más completa:

del tipo decimal es de 16 bytes mientras que el tipo int es de 4 bytes (casi siempre), así que lo que sucede es que el runtime toma la parte entera del tipo decimal y la coloca en los 4 bytes enteros, pero atención, eso solo siempre y cuando la parte entera del dato tipo decimal quepa en 2^32 , de lo contrario se genera una excepción de overflow.

 

Si utilizas el Convert.ToInt32

Explicación básica:

Cuando el método estático Convert.ToInt32()  (en adelante solo ToInt32) recibe como parámetro un tipo de dato Decimal hace más que solo retornar la parte entera, de hecho redondea el resultado al entero más cercano. Esto genera una sobrecarga adicional que lo hace mucho más lento.

Explicación más completa:

ToInt32 cuando recibe un tipo Decimal llama al método interno Decimal.FCallToInt32 que a su vez es un wrapper de un método implementado en código nativo que hace los cálculos necesarios para producir  un valor redondeado. Como se observa entonces hay invocación a 3 métodos como mínimo lo cual implica 3 cambios de contexto: el primero al llamar a ToInt32, luego al llamar a Decimal.FCallToInt32 y luego cuando este hace el wrapper a la función de código nativo, hasta allí ya es mucho más lento que simplemente usar el cast (int), ahora ,  la lógica usada internamente para convertir el tipo de 16 bytes al tipo de 4 bytes y tener en cuenta el redondeo y los casos especiales como por ejemplo cuando el parámetro es null, convierte a esta función en una muchísimas veces más lenta que solo hacer (int).

 

2 – CONVERTIR DE TIPO FLOAT (SINGLE) A TIPO INT (INT32):

Si utilizas el operador de cast, es decir : (int)

Explicación básica:

El runtime lo que hará es simplemente tomar la parte entera del número descartando completamente la parte decimal.

Explicación más completa:

del tipo float es de 4 bytes mientras que el tipo int también es de 4 bytes (casi siempre), así que lo que sucede es que el runtime toma la parte entera del tipo float y la coloca en los 4 bytes enteros.

Si utilizas el Convert.ToInt32

Explicación básica:

Cuando el método estático ToInt32() recibe como parámetro un tipo de dato Single (float) hace más que solo retornar la parte entera, de hecho redondea el resultado al entero más cercano. Esto genera una sobrecarga adicional que lo hace mucho más lento.

Explicación más completa:

ToInt32 cuando recibe un tipo Single (float) llama al método interno Convert.ToInt32 (si, de nuevo, pero llama al que recibe parámetro tipo double ya que la lógica para double es la misma que para Single (float)) que es finalmente quien resuelve la lógica para hacer el redondeo. Como se observa entonces hay invocación a 2 métodos mínimo lo cual implica 2 cambios de contexto: el primero al llamar a ToInt32(Single), luego al llamar a ToInt32(double) que al final es que tiene en cuenta el redondeo y los casos especiales como por ejemplo cuando el parámetro es null, convierte a esta función en una muchísimas veces más lenta que solo hacer (int) pero comparativamente con el caso de Decimal no es tan lenta.

Conclusión: Si necesitas velocidad siempre usa operadores de cast, ejemplo: (int); si requieres aproximación y que te tengan en cuenta los casos especiales usa siempre Convert.

Bookmark and Share

Optimización de Código – Utilizando Generics – C#

April 19th, 2010 by JuanK

Habitualmente en el proceso de desarrollo de nuestros procesos de desarrollo de software hacemos uso de lo comúnmente llamado Colecciones, es decir arreglos de diferente para solucionar determinados problemas.

Normalmente esto no representa mayor problema hasta que nos encontramos con entornos reales de trabajo, aplicaciones cliente servidor o paginas web accesadas de manera simultanea por decenas o miles de usuarios, o como caso mas crítico un videojuego donde uno o dos usuarios (o a veces decenas o miles de ellos…)realizan millones de operaciones por segundo en nuestros procesadores gráficos y CPU’s.

Las colecciones en estos casos se empiezan a volver un problema, porque?. Bien habitualmente muchos de los elementos que utilizamos en el desarrollo son tipos valor como por ejemplo long (Int64), int (Int32), float(Single), double (Double), estructuras (struct) o enumeraciones (enum) los cuales utilizamos ampliamente con el fin de agilizar el rendimiento de nuestra aplicación, pero lastimosamente en el proceso de incluir estos tipos de dato en una colección estos son encajonados (en adelante se hablará boxing) en un tipo object; Esto se debe a que las colecciones para poder almacenar cualquier valor dentro de si almacenan tipos object.

Gracias a Dios o en lo que sea que crean uds.) el CLR 2.0(conocido por la mayoría como .net Framework 2.0) soporta el uso de Generics los cuales no son mas que una cualidad que permite la implementación de código sin tener totalmente definido el/los tipo(s) de dato que esos metodos usan… pero bueno la idea de este hilo no es hacer un curso de generics, así que en resumen gracias a los generics existen un conjunto de colecciones genericas que se traducen en colecciones a las cuales se les puede indicar que tipo de dato almacenaran (entre otras cosas) lo cual se convierte en la eliminación del proceso de boxing y unboxing para poder manipularlas.

Como acotación adicional los desarrolladores de C++ conocen los generics de la STL (Standard Template Library) por lo cual no suelen llamarlos generics sino templates.

Utilizar generics SI es mucho más rápido

Bien, se preguntaran el porque del titulo… así que antes de entrar en materia les contare…

En varios textos de programación en C# y java los autores suelen utilizar unos recuadros con casos reales o notas al margen, y aunque no se el numero exacto si se que ya son varias veces que he visto mencionar que en la practica las colecciones que usan generics no se muestran mucho mas rápidos que las que utilizan el un/boxing tradicional… lo cual es mentira y me pregunto… en que proyectos han trabajado ellos? tal ves en proyecos web y seguramente no en los grandes proyectos web… lo que sin lugar a dudas es cierto es que un desarrollador que tenga experiencia en el montaje de grandes portales, de aplicaciones cliente servidor con gran numero de usuarios o programadores de videojuego o de aplicaciones científicas los desmentirían por completo. Y este post es para eso para desmentir esa falsa creencia y mostrar la realidad: Las colecciones genericas son mucho más eficientes que las no genéricas.

Demostración

Lo primero que haremos sera crear una pequeña aplicación por consola en C# en la cual declararemos esta constante y la siguiente función:

private static long Boxing_Unboxing()
{
    long suma = 0;
    ArrayList arrayEnteros = new ArrayList();
 
    for (int i = 0; i < tamano; i++)
        arrayEnteros.Add(i);
 
    for (int i = 0; i < tamano; i++)
        suma = suma + (int)arrayEnteros[i];
    
    return suma;
}

No requiere mucha explicación, esta función efectúa el llenado (boxing) de una colección con un numero determinado de elementos y seguidamente suma todos estos elementos en un acumulado (ahí hace unboxing) el cual retorna.

Ahora adicionaremos esta función que hace exactamente lo mismo pero utilizando colecciones genericas:

private static long Generics()
{
    long suma = 0;
    List<int> arrayEnteros = new List</int><int>();
 
    for (int i = 0; i &lt; tamano; i++)
        arrayEnteros.Add(i);
 
    for (int i = 0; i &lt; tamano; i++)
        suma = suma + arrayEnteros[i];
    
    return suma;
}
</int>

Ahora en el método main crearemos el código necesario para invocar las dos funciones anteriores, adicionalmente mediremos el tiempo de ejecución de las mismas para lo cual utilizaremos System.Environment.TickCount y finalmente incluiremos código para medir el consumo de memoria de la aplicación para lo cual haremos uso de dos metodos de la clase estática GC, GC.GetTotalMemory para saber el consumo total de memoria en un momento dado y GC.Collect para liberar los recursos que ya no estan en uso. El código queda así:

const int  tamano = 30000000;
static void Main(string[] args)
{
    int tiempo =0;
 
    tiempo = System.Environment.TickCount;
    long suma = Boxing_Unboxing();
    tiempo = System.Environment.TickCount - tiempo;
    Console.WriteLine(&quot;Resultado: &quot;+suma.ToString()+&quot; Tiempo Un/Boxingt:&quot;+tiempo.ToString().PadLeft(10,' ')
                    + &quot; Memoria: &quot;+GC.GetTotalMemory(false).ToString().PadLeft(7,' '));
 
    GC.Collect();
    
    tiempo = System.Environment.TickCount;
    suma = Generics();
    tiempo = System.Environment.TickCount - tiempo;
    Console.WriteLine(&quot;Resultado: &quot; + suma.ToString() + &quot; Tiempo Genericst:&quot; +tiempo.ToString().PadLeft(10, ' ')
                     +&quot; Memoria: &quot; + GC.GetTotalMemory(false).ToString().PadLeft(7, ' '));
    GC.Collect();
    Console.ReadLine();
}

Una vez ejecutado obtendremos los siguientes valores (o muy parecidos dependiendo del computador de cada cual):

Resultado: 199999990000000 Tiempo Un/Boxing : 4695 Memoria: 883595136
Resultado: 199999990000000 Tiempo Generics  :  764 Memoria: 202279736

Como ven en los resultados, generics se muestran aproximadamente 6 veces más rápido que la técnica tradicional y fue casi 5 veces mas efectivo en el uso de la memoria.

Como desconozco que hardware tienen ustedes, pueden hacer pruebas modificando el valor de la constante tamano de acuerdo alas características de su procesador y memoria, esto porque si exceden el limite de almacenamiento en memoria de sus máquinas el sistema operativo iniciara el proceso de swap a disco poniendose la cosa muuuy lenta… ahí obtendrán diferencias de tiempo mucho mayores incluso proporcionalmente.

Hasta la próxima.

Bookmark and Share

Optimización de Código – Cómo Convertir un Entero en Binario – C#

April 18th, 2010 by JuanK

El título de este post debería ser realmente: "Cómo formatear una cadena para mostrar un entero en formato binario"… pero créanme que nadie lo buscaría así en un buscador :P .

Bien esto parece ser un problema recurrente alrededor de los foros en internet, existen muchas soluciones diferentes a este problema, incluso hay algunos lenguajes que ya incluyen soporte para hacer esto fácilmente,  el CLR no se caracteriza por tener una funcionalidad fácil de utilizar sin embargo en este artículo exploraremos varias soluciones (desde luego no todas )posibles hasta llegar a la solución ideal que espero les sea de provecho a todos.

Estas son las opciones que exploraremos, son básicamente 2.

La primera opción no requiere de mucho análisis pero tiene sus inconvenientes digamos menores. La segunda opción es la que más nos importa, aprenderemos a deducir el algoritmo a implementarlo y subsecuentemente lo iremos optimizando hasta lograr una versión digna de un excelente programador:

  1. Convertir con BitVector32 (mala solución pero rápida de implementar)
  2. Convertir creando un algoritmo eficiente
    • Utilizando string
    • Utilizando StringBuilder
    • Utilizando punteros con codigo inseguro

 

1.  CONVERTIR CON BITVECTOR32

Esta es la solución más rápida al problema, pero en mi opinión la menos profesional….

BitVector32 es una colección especializada que se encuentra en:

System.Collections.Specialized

Esta clase provee una estructura simple que almacena valores boléanos que representan un entero de 32 bit.  Que que?

Ok esta explicación no necesariamente es clara para todos, hagámoslo muy simple veamos un entero de 32 bits, imaginemos que en memoria se ve así:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

Es decir cada casilla representa un bit, para completar así 32 bits.

Bien entonces esto a final de cuentas es una colección bits, es decir en cada ‘celda’ podemos tener dos valores posibles 1 o 0… true o false.

Bien eso es lo hace BitVector toma un entero y lo permite manejar como un array de booleanos.

El método ToString() permite convertir todo esto a 1s y 0s para poder ver los valores de cada ‘celda’ en su representación numérica, sin embargo esto tiene un inconveniente pues si utilizamos elObjetoBitVector.ToString() recibiremos algo así como esto:

BitVector32{00000000000000000000000001101111}

Nos sobra algo al principio y al final… a nosotros solo nos interesan los datos binarios así que la solución es hacer un substring de la parte de la cadena que nos interesa, finalmente el código quedaría así:

public static string EB_BitVector32(Int32 entero)
{
    BitVector32 bv = new BitVector32(entero);
    return bv.ToString().Substring(12, 32);
}

Esto nos produciría este resultado similar a:

00000000000000000000000001101111

 

2.  CONVERTIR CREANDO UN ALGORITMO EFICIENTE

Ahora si entremos en materia hagamos el algoritmo.

Una de las primeras ideas que a uno se le viene a la mente es utilizar el conocido algoritmo que nos permite convertir un numero decimal a base 2, para el que no lo recuerde o no lo conozca: es un algoritmo que toma como base divisiones anidadas del valor entre 2 y recupera los residuos. En este link tienen una explicación detallada al respecto: Decimal a Binario

Sin embargo dicho algoritmo puede ser ineficiente para nuestro gusto.

Retomemos conceptos como puntos clave que nos permitan llegar a un algoritmo óptimo:

  1. Básicamente un entero es un arreglo de 4 bytes (32 bits)
  2. Por cada bit se debe producir su equivalente como cadena ("0" , "1") según si el bit esta prendido(1) o apagado(0)
  3. Podemos acceder a todos los bit como un arreglo de boolean a través de BitVector32, pero en ese caso la solución sería aún más lenta que utilizando la propuesta inicial que usa BitVector32
  4. Hay que encontrar el mecanismo adecuado para recorrer los bit

De los conceptos clave tenemos que los dos primeros nos son útiles, el tercero no nos aporta nada nuevo y el cuarto es nuestra necesidad más inmediata: Averiguar como recorrer los bit sin necesidad de usar BitVector32.

Lo primero que me salta a la mente es usar operadores binarios especialmente corrimientos de bit (<<) y and (&) para las máscaras.

 

Veamos un poco de teoría acerca de lo que haremos.

  1. Crear un bucle e 32 ciclos
  2. Leemos siempre el valor del primer bit del entero de 32 bit, esto lo podemos hacer efectuando un and binario (&) del entero con un numero que tenga el primer bit prendido únicamente, es decir con : 0×80000000 (es decir 2147483648 )… porqué este número?  bueno coloquemos este 0×80000000 en nuestro arreglo de 32 bits:
    1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    8       0       0       0       0       0       0       0      

    Cada 4 bits representan uno de los dígitos hexa, de esta forma el 0×8 esta representado por los primeros 4 bits, en efecto 1000 = 0×8, así que si se fijan en nuestro arreglo de bits solo tenemos un bit prendido, de tal forma que cualquier cosa que haga & con 0×80000000 solo podría llegar a tener prendido el bit 1, así podemos hallar que valor tiene el bit 1 de un número dado ya que si ese bit esta en 0 al hacer and quedaría en 0, mientras que si esta en 1 al hacer and quedaría en uno nuevamente. Concretando, cuando hacemos un and entre dos enteros y uno de los enteros es 0×80000000 el resultado siempre será el valor del primer bit del otro entero. ( espero haber sido claro).

  3. En cada iteración corremos 1 bit posición a la izquierda (<< 1), de tal forma que el primer bit es reemplazado por el bit siguiente
  4. Se regresa al paso 2

IMPLEMENTACION CRUDA Utilizando String

Este es el código resultante de la implementación más simpe del algoritmo:

public static string EB_String(int entero)
{
    //La máscara y el # de iteraciones
    const uint mascara = 0x80000000;
    const int iteraciones = 32;
    //el contador y el resultado
    int contador = 0;
    string resultado = &quot;&quot;;
 
    //Se recorren los 32 bit
    while (contador++ &lt; iteraciones)
    {
        /*Si el entero and la mascara = 0 quiere decir
         *que el bit 1 esta apagado*/
        if ((entero &amp; mascara) == 0)
            resultado += &quot;0&quot;;
        else
            resultado += &quot;1&quot;;
 
        /*correr un bit a la izquierda para poner
         *el siguiente bit en la posicion del primero*/
        entero = entero &lt;&lt; 1;
    }
    return resultado;
}
 

OPTIMIZACIÓN DE CÓDIGO Utilizando StringBuilder

La implementación anterior es susceptible de una sencilla mejora utilizando StringBuilder ya que al ser string un tipo inmutable , las recurrentes concatenaciones generadas al utilizar masivamente el método sobrecargarían el GC, sin contar el overhead producido por las frecuentes reservas de memoria (1 adicional por cada cambio al string). StringBuilder se encuentra en el namespace System.Text.

Código optimizado utilizando StringBuilder:

public static string EB_StringBuilder(int entero)
{
    //La máscara y el # de iteraciones
    const uint mascara = 0x80000000;
    const int iteraciones = 32;
    //el contador y el resultado
    int contador = 0;
    StringBuilder resultado = new StringBuilder(iteraciones);
 
    //Se recorren los 32 bit
    while (contador++ &lt; iteraciones)
    {
        /*Si el entero and la mascara = 0 quiere decir
         *que el bit 1 esta apagado*/
        if ((entero &amp; mascara) == 0)
            resultado.Append('0');
        else
            resultado.Append('1');
 
        /*correr un bit a la izquierda para poner
         *el siguiente bit en la posicion del primero*/
        entero = entero &lt;&lt; 1;
    }
    return resultado.ToString();
}

OPTIMIZACIÓN DE CÓDIGO Utilizando punteros – código unsafe

Aunque StringBuilder ya proporciona una mejora muy importante, dadas las características de nuestro problema(concatenación uno a uno) resulta mucho más idóneo generar código que utilice punteros. Es importante tener en cuenta que para que este código funcione se requiere modificar las propiedades del proyecto para que admita código unsafe.

Esta es la versión básica con manejo de apuntadores:

public static unsafe string EB_Unsafe(int entero)
{
    const uint mascara = 0x80000000;
    const int iteraciones = 32;
 
    int contador = 0;
 
    //Se reservan 32 posiciones y uno adicional para 
    //terminacion en null
    char* resultado = stackalloc char[iteraciones + 1];
    //puntero de trabajo
    char* aux = resultado;
 
    while (contador++ &lt; iteraciones)
    {
        if ((entero &amp; mascara) == 0)
            *aux = '0';
        else
            *aux = '1';
 
        //Mover el puntero una posicion dentro de la cadena
        aux++;
 
        entero = entero &lt;&lt; 1;
    }
    return new string(resultado);
}

Esta es la versión final de "bonus" totalmente optimizada para velocidad de procesamiento, es levemente diferente de la anterior para realizar la menor cantidad de validaciones posibles y para que ignore los 0s a la izquierda, lo cual la hace excepcionalmente rápida en la mayoría de los casos. Revísenla…:

public static unsafe string EB_Unsafe_Opt(int entero)
{
    const uint mascara = 0x1;
    const int iteraciones = 32;
 
    char* resultado = stackalloc char[iteraciones + 1];
    char* aux = resultado + iteraciones - 1;
 
    do
    {
        if ((entero &amp; mascara) == 0)
            *(aux--) = '0';
        else
            *(aux--) = '1';
 
        entero = entero &gt;&gt; 1;
    } while (entero != 0);
 
    return new string(++aux);
}

Si alguien tiene una solución mejor por favor no dude en compartirla!!

En unos días publicare un programa especialmente diseñado para medir la velocidad de cada una de estas alternativas… les adelanto que las diferencias de tiempo van casi hasta de 20 o 30 veces más velocidad.

 

Happy Learning!!!

OOOHH OLvidaba decirles Si quieren pueden usar:

System.Convert.ToString(elValor, 2);

jajajajaja, bueno el ejercicio que hicimos produce un resultado 30% más rápido. :P

Bookmark and Share

« Previous Entries