Mostrando entradas con la etiqueta mejora de rendimiento. Mostrar todas las entradas
Mostrando entradas con la etiqueta mejora de rendimiento. Mostrar todas las entradas

lunes, 24 de junio de 2013

Mejorar la calidad de código como manera de optimizar el rendimiento

Artículo perteneciente a la sección de calidad del código

En muchas ocasiones oiréis (o incluso diréis): "he tenido que hacer esta guarrada para poder optimizar este caso concreto; se que no es demasiado bonito, pero era la única manera de optimizarlo". Tenemos metido en la cabeza, desde que empezamos a programar, que los códigos o son mantenibles o son óptimos, cuando la verdad es que normalmente cuando "optimizamos" siempre enguarramos el código mientras que cuando mejoramos la mantenibilidad del código casi siempre mejoramos el rendimiento de nuestro software, casi casi sin quererlo.

Es cierto que esta mejora del rendimiento es genérica y se esparce de igual manera por todo nuestro código y que tal vez no afecte tanto como nos gustaría a nuestra parte más crítica del programa. En este caso, cuando ya hemos hecho todo lo posible para mejorar la calidad del código, es cuando es lícito que apliquemos optimizaciones (como el uso de asm) a las zonas críticas, pero no antes.

El motivo por el cual la mejora de la calidad de código mejora nuestro rendimiento es sencillo. Mejorar la calidad de nuestros programas tiene dos componentes muy marcados: reducir duplicaciones de código(refactorizar) y disminuir la complejidad ciclomática.

Disminuyendo la complejidad ciclomática disminuimos el número de condicionales de nuestras funciones, es decir, reducimos cases dentro de los switchs y reducimos if's. Estas funciones condicionales son de las que más impactan en el rendimiento de nuestro código ya que son las que rompen el pipeline del procesador (lo explicaremos en otro artículo con más calma)  y hacen que el rendimiento de este baje en picado, por lo tanto, reduciendo condicionales aumentamos de forma directa la velocidad de proceso de nuestra aplicación.

Disminuyendo las duplicaciones de código por un lado permitimos que nuestras funciones quepan mejor en la cache de la CPU (nuevamente, lo explicaremos en otro artículo) haciendo que la velocidad de ejecución pueda aumentar drásticamente y por otro lado nos aseguramos que las optimizaciones que posteriormente se quieran implementar se harán solo una vez, evitando fallos y malfuncionamientos.

De resultas de estas dos características se acostumbra a tener un código más pequeño y con menos llamadas innecesarias a funciones, haciendo que el rendimiento de nuestra aplicación también aumente por este motivo.

A modo de ejemplo, imaginemos que tenemos una zona de código donde hemos detectado un cuello de botella. El problema en concreto reside en que tenemos una función que nos devuelve el modulo 10 del número que le pasamos como parámetro (recordad que solo es un ejemplo) y ha de hacerlo muchas veces y mur rápido para cumplir con las especificaciones que nos piden.

int mod_original(int i)
{
return (i%10);
}

Un programador con algo de experiencia sabe que según que operaciones matemáticas cuestan lo suyo por lo tanto, aunque sea una guarrada se podría implementar un vector precalculado o un switch con  todos los casos que nos afecten para no tener que realizar la operación costosa que nos atañe. El código de ejemplo sería este (seguid recordando que esto es un ejemplo :) )

int mod_switch(int i)
{
switch(i)
{
case 0: return 0;
case 1: return 1;
case 2: return 2;
case 3: return 3;
case 4: return 4;
case 5: return 5;
case 6: return 6;
case 7: return 7;
case 8: return 8;
case 9: return 9;
case 10: return 0;
default: return 0;
}
}

Con esta guarrada conseguiriamos mejorar algo el rendimiento, pero a coste de empeorar muchisimo la mantenibilidad.Aparte estariamos introduciendo un error ya que si en el futuro quisieramos usar esta función para calcular el modulo 10 de un número superior a 10 no funcionaria correctamente(default: return 0;). Entonces es cuando un segundo programador con más experiencia o más sentido común debería decir que ese tipo de guarradas no deberían hacerse y buscar otra solución. Al darle vueltas al problema no solo encontraría una forma mejor de implementar la función sino que se daría cuenta que el coste real no estaría en la operación de módulo sino en la llamada a la función, así que propondría una solución como la siguiente:

#define mod_opt(i) (i%10)

Cúal de los tres códigos es el más legible? Y cual el más eficiente?

En cuanto a legibilidad, para mi el tercero (aunque entiendo que es una cuestión personal) y en cuanto a eficiencia, os dejo los valores reales de la ejecución de los tres códigos:

Original:
Case:0 Milis:34
Case:1 Milis:30
Case:2 Milis:29
Case:3 Milis:30
Case:4 Milis:30
Case:5 Milis:30
Case:6 Milis:30
Case:7 Milis:30
Case:8 Milis:34
Case:9 Milis:34
Switch:
Case:0 Milis:29
Case:1 Milis:29
Case:2 Milis:28
Case:3 Milis:29
Case:4 Milis:28
Case:5 Milis:29
Case:6 Milis:28
Case:7 Milis:29
Case:8 Milis:29
Case:9 Milis:28
Opt:
Case:0 Milis:3
Case:1 Milis:3
Case:2 Milis:3
Case:3 Milis:3
Case:4 Milis:3
Case:5 Milis:3
Case:6 Milis:3
Case:7 Milis:3
Case:8 Milis:4
Case:9 Milis:4

Creo que no hace falta decir que en este caso , como en otros muchos, mejorar la legibilidad y la mantenibilidad del código no solo son buenas prácticas de programación sino que permiten mejorar el rendimiento de nuestras aplicaciones de forma directa.

Espero que os haya gustado.

Si tenéis dudas no tengáis reparos hacérmelas llegar.

Nos vemos


LordPakusBlog

lunes, 5 de septiembre de 2011

Math Engine : Capitulo 6. Vector.distance con SSE (x10 al rendimiento)

Capítulo perteneciente al math engine LordPakus

Hola a todos

No se si recordareis que Thathi me pidió en el capitulo 2 que implementara la distancia entre dos vectores. y que tal y como os dije en el capitulo 5 cuanto más compleja es la operación más mejora de rendimiento se puede llegar a conseguir.

Pues bien, concretamente para la optimización SSE, para el caso de la distancia entre dos vectores se ha conseguido unos valores de tiempo de :
Version sin optimizar               Tiempo gastado: 680milisegundos
Version optimizada con SSE   Tiempo gastado: 68milisegundos

Es decir se ha CONSEGUIDO MULTIPLICAR POR 10 EL RENDIMIENTO!!!. (os lo dije :D)

Aqui os dejo el código para que veais  como lo he hecho:

float LPVector::distanceOld(LPVector vector)
{
    float a = 0.0;
    float temp = 0.0;

    //Si las dimensiones son diferentes , tenemos un problema grave.
    if (n != vector.n)
    {
        cout << "ERROR DIMENSIONAL ENTRE VECTORES\n";
        return (-1.0);
    }

    for(int i = 0 ; i < n ; ++i)
    {
        temp = (v[i] - vector.v[i]);
        a += ( temp * temp ) ;
    }

    return sqrt(a);
}

float LPVector::distanceSSE(LPVector vector)
{
    //Fast SSE code when float specified
    float b[4];
    float* const row0 = (float*) &v[0];
    float* const row1 = (float*) &(vector.v[0]);
    float* const a    = (float*) &b[0];
    float temp = 0.0;
    int i = 0;

    //Si las dimensiones son diferentes , tenemos un problema grave.
    if (n != vector.n)
    {
        cout << "ERROR DIMENSIONAL ENTRE VECTORES\n";
        return (-1.0);
    }

    __asm
    {
        // Carga trozos de los vectores de 4 en 4
        // La carga se hace "desordenada para que siempre haya una instrucción como mínimo entre uso y uso de registros."
        mov      edx, row0
        mov      esi, row1
        mov         edi, a

        //Al hacer una XOR ponemos el registro a 0 más rapidamente que hacciendo un mov de 0
        //El registro xmm7 (el último) lo ponemos de variable acumuladora de resultado
        xorps        xmm7, xmm7
    }

    i = n;
   
    while( i >= 12 )
    {
        __asm
        {
            //Cargamos la info en los registros SSE
            movups   xmm0, [edx]
            movups   xmm1, [esi]

            //Aumentamos los contadores en 16 = 4 elementos * 4 bytes por elemento (float)
            add         edx,16
            add         esi,16

            //Cargamos la info en los registros SSE(tenemos 8 registros donde guardar info)
            movups   xmm2, [edx]
            movups   xmm3, [esi]

            //Aumentamos los contadores en 16 = 4 elementos * 4 bytes por elemento (float)
            add     edx,16
            add  esi,16

            //Cargamos la info en los registros SSE(tenemos 8 registros donde guardar info)
            movups   xmm4, [edx]
            movups   xmm5, [esi]

            //Aumentamos los contadores en 16 = 4 elementos * 4 bytes por elemento (float)
            add     edx,16
            add  esi,16

            //Hacemos la operación que toca, en nuestro caso, restar , elevar el resultado al cuadrado y acumular el resultado
            subps    xmm0 , xmm1    //Restamos de 4 en 4
            subps    xmm2 , xmm3    //Restamos de 4 en 4
            subps    xmm4 , xmm5    //Restamos de 4 en 4

            //Elevamos al cuadrado la diferencia
            mulps    xmm0,xmm0
            mulps   xmm2,xmm2
            mulps    xmm4,xmm4

            //Sumamos resultados parciales al total (xmm7)
            addps    xmm7,xmm0
            addps    xmm7,xmm2
            addps    xmm7,xmm4
        }
        i -= 12;
    }
   
    __asm
    {
        movups     [edi], xmm7
    }
   
    b[0] += (b[1] + b[2] + b[3]);

    //Los flecos los rematamos de la manera tradicional
    for(int j = n-i ; j < n ; ++j)
    {
        temp = (v[i] - vector.v[i]);
        b[0] += temp*temp;
    }

    return sqrt(b[0]);

}

int main (int argc, char* argv[])
{
    LPVector v1,v2;
    float d1,d2;
    clock_t timer;
    float delay;

    //Creamos los vectores aleatorios
    v1.random(DIM,0.0f,100.0f);
    v2.random(DIM,0.0f,100.0f);

    //Sumamos los vectores
    cout << "Version sin optimizar\n";
    timer = clock();
    for(int i=0 ; i < 100000 ; i++ )
        d1 = v1.distanceOld ( v2 );
    delay = ( clock() - timer ) ;
    cout << "Tiempo gastado: " << delay << "milisegundos\n";

    cout << "Version optimizada con SSE\n";
    timer = clock();
    for(int i=0 ; i < 100000 ; i++ )
        d2 = v1.distanceSSE ( v2 );
    delay = ( clock() - timer ) ;
    cout << "Tiempo gastado: " << delay << "milisegundos\n";

    if ( d1 != d2)
        cout << "Error: La operacion da diferente resultado!!!\n";

    if ( d1 == d2)
        cout << "Calculo correcto\n";

    //Eliminamos los vectores
    v1.Delete();
    v2.Delete();

    system("PAUSE");
}

Si hay algo que no entendais o quereis que implemente en SSE alguna función en especial no dudeis en hacerme llegar vuestras sugerencias o preocupaciones...

Espero que os haya gustado y que hayais aprendido...

Nos vemos

Entradas populares