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, 10 de junio de 2017

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

Así que, ¿dónde estábamos? Ah sí, en el último post (que acabáis de leer otra vez, ¿verdad?) hemos aprobado (con dudas) los VLAs, que son fáciles de usar, útiles y con un rendimiento excelente, pero entonces... ¿por qué dije que eran aptos para el rol del malo en el legendario "El bueno, el feo y el malo"?
no confiáis en el VLA, ¡os lo dice el bueno!
Así de fácil: además de los (considerables) pros también hay algunos (pesados) contras. Antes de seguir siempre hay que recordar que un VLA se asigna dinámicamente en el stack, como una variable automática con scope limitado al bloque de código en la que la alocacion se lleva a cabo: considerando esto los (principales) problemas posibles son:
  1. la gestión de errores es problemática, porque no hay forma de saber si el VLA ha sido alocado bien o ha causado un stack overflow (en este caso provoca efectos similares a los de un error para recursión infinita).
  2. el tamaño del VLA se decide en run-time, por lo que el compilador debe hacer juegos un poco extraños: dependiendo de la aplicación, es posible que una parte (a veces importante) del stack de una función se reserve para un VLA, limitando mucho la memoria local disponible. O sea: el stack overflow està siempre alrededor de la esquina.
  3. la portabilidad del código se desmorona un poco: el código se vuelve muy compiler-dependent y, sobre todo, ya que una buena parte de los programadores C escriben también código para sistemas embedded (donde el stack está, a menudo, limitado) resulta complicado el porting de funciones de aplicaciones normales a aplicaciones embedded. Funciones que, tal vez, dejarían de funcionar por razones misteriosas (bueno, no tan misteriosas).
  4. Por último, pero no menos importante: tal vez por las razones mencionadas anteriormente (u otras más) de C11 hacia adelante los VLAs son opcionales y están sujetas a una variable del compilador __STDC_NO_VLA__: mala señal.
Entonces, ¿qué? Mejor no usarlos o utilizarlos con las precauciones necesarias, porque las alternativas no faltan. ¡Malo encontrado!

Y ahora tenemos que buscar a alguien que se adapte a los papeles de bueno y feo. Vale, para el bueno no hay problema, el candidato ideal es la nuestra querida amiga malloc(), que es siempre una garantía y ha salido muy bien de los test. Sobre malloc() es inútil gasta mas palabras, es un punto fijo del C y ya hemos hablado a fondo sobre ella aquí.

Y el feo? Bueno, para encontrar uno adecuado tendremos, por desgracia, entrar en el lado oscuro de la fuerza, es decir, en el territorio C++...

(...Abro un paréntesis: nunca hablo de temas que no conozco, porque creo que es estúpido hacerlo. Por ejemplo: no entiendo nada de motos y, se lo aseguro, nadie ha tenido el honor de escucharme charlar sobre el mundial de MotoGP. Sigo una filosofía, que, por desgracia, no es seguida por muchas personas, o sea: "mejor callar que hablar sólo para dar aire a la boca". Precisamente gracias a esta coherencia creo que tengo las cualidades para hablar de C++: yo lo he usado en paralelo a mi amado C para casi treinta años (!), y, modestia aparte, creo que se usarlo muy bien. Así que puede hablar sobre el, para bien o para mal. Cierro el paréntesis...).


Entonces, he retomado el ejemplo C del post anterior y (haciendo el mínimo sindical de modificaciones) le he transformado en codigo C++, con el fin, por lo tanto, de añadir un nuevo test que usa std::vector (esto es un objeto particularmente querido para los C++ lovers, que lo usan también para condimentar la ensalada). Para evitar de repetir todo el código del ultimo post os paso sólo el main() y la nueva función de test (el resto es, prácticamente, idéntico). ¡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 testVectorVLA(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, &testVectorVLA, MYSIZE, "testVectorVLA");

    // sale
    return EXIT_SUCCESS;
}

// función testVectorVLA()
void testVectorVLA(
    int size)       // size para std::vector
{
    std::vector<int> vectorvla(size);

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

    // instrucción con el fin de evitar el vaciado total de las funciones usando G++ -O2
    avoid_optimization = vectorvla[size / 2];
}
Compilando (con/sin optimizaciones) y ejecutando este código los resultados son los siguientes:
aldo@ao-linux-nb:~/blogtest$ g++ vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tiempo transcurrido: 4.274441 segundos
testMallocVLA -  Tiempo transcurrido: 3.641508 segundos
testStackFLA  -  Tiempo transcurrido: 4.340430 segundos
testHeapFLA   -  Tiempo transcurrido: 4.312986 segundos
testVectorVLA -  Tiempo transcurrido: 10.660610 segundos
aldo@ao-linux-nb:~/blogtest$ g++ -O2 vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tiempo transcurrido: 0.768702 segundos
testMallocVLA -  Tiempo transcurrido: 0.694418 segundos
testStackFLA  -  Tiempo transcurrido: 0.682241 segundos
testHeapFLA   -  Tiempo transcurrido: 0.694299 segundos
testVectorVLA -  Tiempo transcurrido: 1.364321 segundos
¿Cómo se ha portado std::vector? Yo diría que las cifras hablan por sí solas... bien, vamos a ponerlo en una forma diplomática: digamos que tenemos dos noticias, una buena y una mala:
  • la buena noticia es que el C++ es tan eficiente como el C (y sobre esto no tenía ninguna duda), de hecho el nuestro programa C transformado en C++ obtiene (en los primeros cuatro test) el mismo rendimiento (¡ir a controlar ahi, si no me creéis, eh!).
  • la mala noticia es que el C++ es tan eficiente como el C, pero sólo si lo usas como el C, entonces nada de STL y parafernalias varias.
(...Abro otro paréntesis: por supuesto, la mala noticia anterior no se deriva sólo del simple test propuesto en este post: se deriva de años y años de observaciones y uso intensivo de los dos lenguajes, solo faltaría. Cierro el paréntesis...).

Sin entrar mucho en detalles (tal vez un día voy a escribir un post específico sobre el tema) os presento mi opinión: el lenguaje C++ es un gran lenguaje potente, eficiente y expresivo (¡es pariente cercano del C!), con el cual se puede escribir Software de alta calidad. Pero los mejores resultados (al menos en términos de rendimiento y fluidez del código) se obtienen mediante el uso por lo cual fue diseñado originalmente, es decir, como un C a objetos. El cambio que tuvo más tarde (desde cuando cayó en manos de los committee ISO) no me gusta y no me convence... pero, por suerte (y esto es importante), todavía se puede utilizar en su esencia, la que permite escribir a objetos utilizando un lenguaje (casi) igual al C (y esto corresponde a la buena noticia aqui arriba. Obviamente, si rendimiento y fluidez del código no se consideran factores importantes, entonces todas estas consideraciones pierden de significado...).

Ah, un último punto para los que se han sorprendido por el código C++ (aquí arriba) que incluye un VLA: es una amable oferta de nuestro amado GCC (en su encarnación G++). Por lo tanto es una extensión del lenguaje proporcionada por el compilador, ya que los VLAs no forman parte del estándar C++ (incluso las últimas versiones C++11 y C++14).

En el próximo posta, para cerrar el círculo, vamos a hablar de un pariente cercano de los VLAs, o sea de la función alloca(). ¿Será otro bueno, otro feo, u otro malo?

¡Hasta el próximo post!