En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

sábado, 8 de julio de 2017

El bueno, el feo y el VLA
cómo usar los Variable Length Arrays en C - pt.3

Aquí estamos, y, como prometido, este mes vamos a hablar de un pariente cercano de los VLAs, o sea de la función alloca()... ¿será un bueno, un feo o un malo?
¡hola, soy el spoiler de este post!
Entonces, he añadido código en el programa de test para probar la alloca(). Y, para terminar a lo grande, he añadido código para probar la malloc() del C++, o sea la new (después del problemático test de std::vector del último post era correcto acabar el argumento con algo con mejore prestaciones, para que no se diga que tengo algo en contra del C++...). Así que vamos a utilizar programa C++ del post pasado (ya que era prácticamente idéntico a la versión C): os enseño otra vez el main() y las dos funciones de test añadidas (para reconstruir el programa completo es suficiente consultar los dos post anteriores y hacer un poco cut-and-paste). ¡Vamos con el código!
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <vector>

#define MYSIZE  1000000

// variable dummy con el fin de evitar el vaciado total de las funciones usando G++ -O2
int avoid_optimization;

// prototipos locales
void testVLA(int size);
void testMallocVLA(int size);
void testStackFLA(int dum);
void testHeapFLA(int dum);
void testAllocaVLA(int size);
void testVectorVLA(int size);
void testNewVLA(int size);
void runTest(int iterations, void (*funcptr)(int), int size, const char *name);

// función main()
int main(int argc, char* argv[])
{
    // test argumentos
    if (argc != 2) {
        // error args
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s vla iterations [e.g.: %s 10000]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // extrae iteraciones
    int iterations = atoi(argv[1]);

    // ejecuta test
    runTest(iterations, &testVLA, MYSIZE, "testVLA");
    runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA");
    runTest(iterations, &testStackFLA, 0, "testStackFLA");
    runTest(iterations, &testHeapFLA, 0, "testHeapFLA");
    runTest(iterations, &testAllocaVLA, MYSIZE, "testAllocaVLA");
    runTest(iterations, &testVectorVLA, MYSIZE, "testVectorVLA");
    runTest(iterations, &testNewVLA, MYSIZE, "testNewVLA");

    // sale
    return EXIT_SUCCESS;
}

// función testAllocaVLA()
void testAllocaVLA(
    int size)       // size para alloca()
{
    int *allocavla = (int*)alloca(size * sizeof(int));

    // loop de test
    for (int i = 0; i < size; i++)
        allocavla[i] = i;

    // instrucción con el fin de evitar el vaciado total de las funciones usando G++ -O2
    avoid_optimization = allocavla[size / 2];
}

// función testNewVLA()
void testNewVLA(
    int size)       // size para new
{
    int *newvla = new int[size];

    // loop de test
    for (int i = 0; i < size; i++)
        newvla[i] = i;

    // instrucción con el fin de evitar el vaciado total de las funciones usando G++ -O2
    avoid_optimization = newvla[size / 2];

    delete[] newvla;
}
Como se puede ver, las dos funciones añadidas están perfectamente alineadas estilísticamente con las demás que ya había propuesto y son, como siempre, hiper-comentadas, así que ni siquiera tengo que detenerme en largas explicaciones. ¿Y los resultados de las pruebas? ¡Vamos a verlos!
aldo@ao-linux-nb:~/blogtest$ g++ vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tiempo transcurrido: 4.318492 segundos
testMallocVLA -  Tiempo transcurrido: 3.676805 segundos
testStackFLA  -  Tiempo transcurrido: 4.339859 segundos
testHeapFLA   -  Tiempo transcurrido: 4.340040 segundos
testAllocaVLA -  Tiempo transcurrido: 3.678644 segundos
testVectorVLA -  Tiempo transcurrido: 10.934088 segundos
testNewVLA    -  Tiempo transcurrido: 3.679624 segundos
aldo@ao-linux-nb:~/blogtest$ g++ -O2 vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tiempo transcurrido: 0.746956 segundos
testMallocVLA -  Tiempo transcurrido: 0.697261 segundos
testStackFLA  -  Tiempo transcurrido: 0.696310 segundos
testHeapFLA   -  Tiempo transcurrido: 0.700047 segundos
testAllocaVLA -  Tiempo transcurrido: 0.691677 segundos
testVectorVLA -  Tiempo transcurrido: 1.384563 segundos
testNewVLA    -  Tiempo transcurrido: 0.695037 segundos
Entonces, ¿qué se puede decir? Los resultados de los test de los post anteriores ya los hemos ampliamente comentados, por lo que ahora sólo podemos añadir que: alloca() es muy rápida, ya que es, en práctica, una malloc() en el stack (y utilizándola de manera más apropriada, podría/debería ser la más rápida del grupo). ¿Y la new? Bueno, se comporta (como se esperaba) muy bien, también porque, casi siempre, la new internamente utiliza la malloc().

De acuerdo, la alloca() es rápida, pero lo es (sólo un poco menos) también un VLA, y esto no le ha salvado de ser elegido como el malo de la película. Así que vamos a hacer de nuevo una lista de pros y contras, y a ver cuál es el lado más pesado. Veamos primero los pros:
  1. la alloca() es muy rápida, ya que usa el stack en lugar del heap.
  2. la alloca() es fácil de usar, es una malloc() sin free(). La variable alocada tiene un scope a nivel de función, entonces sigue siendo válida hasta que la función retorna al caller, exactamente como cualquier variable automática local (también un VLA funciona más o menos así, pero su scope es a nivel de bloque, no de función, y esto es, probablemente, un punto a favor de los VLAs).
  3. para la razón explicada en la sección 2, la alloca() no deja residuos de memoria en caso de errores graves en las actividades de una función (con malloc() + free() no es tan fácil lograr esto). Y si estás acostumbrado a utilizar cositas como longjmp() las ventajas en este sentido son grandes.
  4. debido a su aplicación interna (sin entrar en más detalles) la alloca() no provoca la fragmentación de memoria. 
    ¡Uh, qué bien! ¿Y los contras?
    1. la gestión de errores es problemática, porque no hay forma de saber si la alloca() ha alocado bien o ha causado un stack overflow (en este caso provoca efectos similares a los de un error para recursión infinita)... uh, esto es exactamente el mismo problema de los VLAs.
    2. la alloca() no es muy portable, ya que no es una función estándar y su funcionamiento/presencia depende en gran medida del compilador en uso.
    3. la alloca() es error prone (parte 1): hay que utilizarla con cuidado, ya que induce, por lo general, a errores cómo utilizar la variable asignada cuando ya no es válida (pasarla con un return o insertarla en una estructura de datos externa a la función, por ejemplo)... pero nosotros somos buenos programadores y este punto no nos da miedo, ¿no?
    4. la alloca() es error prone (parte  2): hay problemas aún más sutiles que considerar en el uso, por ejemplo puede ser MUY peligroso poner una alloca() dentro de un bucle o de una función recursiva (¡pobre stack!) o en una función inline (que utiliza el stack de manera que choca un poco con la forma de utilizar el stack de la alloca()) ... pero nosotros somos buenos programadores y este punto no nos da miedo, ¿no?
    5. la alloca() es error prone (parte 3): alloca() utiliza el stack, que normalmente está limitado con respecto al heap (especialmente en entornos embedded que son muy frecuentados por los programadores C...). Así agotar el stack y causar un stack overflow es fácil (y difícil de controlar, véase el punto 1)... pero nosotros somos buenos programadores y este punto no nos da miedo, ¿no?
      Vale, ¿las conclusiones? Habría motivos para declarar la alloca() como otro malo (la misma suerte que el VLA), pero teniendo en cuenta las importantes ventajas y, sobre todo, porque hoy estoy de buen humor, le declararemos solo como feo (¿habéis visto el spoiler en la imagen arriba?). De todos modos utilizar la alloca() con mucho cuidado, ¡hombre prevenido vale por dos!

      ¡Hasta el próximo post!