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!

viernes, 19 de mayo de 2017

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

La referencia cinematográficas de este mes es ideal: un Variable Length Array (VLA para los amigos) sería perfecto para interpretar el malo en la obra maestra "El bueno, el feo y el malo" del legendario Sergio Leone. Y al final del(proximo) post resultará claro por qué.
...hola soy un VLA: comienza a preocuparte...
Los VLAs son una cosa relativamente nueva del C: se introdujeron en C99, y son, al parecer, el sueño hecho realidad del mundo C: "¡Finalmente hay array con dimensiones variable! ¡Ah, si los hubiera tenido antes '99!". Vale, la idea es simple: con un VLA tipo podéis escribir cositas como estas:
void myVla(
    int size1,
    int size2)
{
    // mi VLA de int
    int ivla[size1];

    // hace algo con el VLA de int
    ...

    // mi VLA bidimensional de float
    float fvla[size1][size2]:

    // hace algo con el VLA bidimensional de float
    ...
}
¿Fantastico, no? Demasiado bueno para ser verdad... pero habrá algunas contraindicaciones? Definitivamente no en el rendimiento: justo por eso he escrito un poco de código para poner a prueba las prestaciones de los VLAs respeto a las alternativas directas: array dinámicos (con malloc()) y array estáticos (en heap y stack). ¡vamos con el código!
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

#define MYSIZE  1000000

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

// prototipos locales
void testVLA(int size);
void testMallocVLA(int size);
void testStackFLA(int dum);
void testHeapFLA(int dum);
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");

    // sale
    return EXIT_SUCCESS;
}

// función runTest()
void runTest(
    int        iterations,      // iteraciones del test
    void       (*funcptr)(int), // función de test
    int        size,            // size del array
    const char *name)           // nombre función de test
{
    // lee start time
    clock_t t_start = clock();

    // ejecuta iteraciones de test
    for (int i = 0; i < iterations; i++)
        (*funcptr)(size);

    // lee end time y enseña el resultado
    clock_t t_end = clock();
    double t_passed = ((double)(t_end - t_start)) / CLOCKS_PER_SEC;
    printf("%-13s -  Tiempo transcurrido: %f segundos\n", name, t_passed);
}

// función testVLA()
void testVLA(
    int size)       // size para VLA
{
    int vla[size];

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

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

// función testMallocVLA()
void testMallocVLA(
    int size)       // size para malloc()
{
    int *mallocvla = malloc(size * sizeof(int));

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

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

    free(mallocvla);
}
 
// función testStackFLA()
void testStackFLA(
    int dum)        // parámetro dummy
{
    int stackfla[MYSIZE];

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

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

// función testHeapFLA()
int heapfla[MYSIZE];
void testHeapFLA(
    int dum)        // parámetro dummy
{
    // loop de test
    for (int i = 0; i < MYSIZE; i++)
        heapfla[i] = i;
}
Aquí, como siempre, corto-y-pego lo que siempre escribo después de mostrar el código (equipo que gana no se cambia...): Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciones y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales.

Así que: dado que es un test comparativo he escrito una función runTest() que llama n-iteraciones de la función a probar y cuenta el tiempo utilizado. el main() llama simplemente cuatro veces runTest(), una para cada función. Las cuatro funciones de test que he escrito prueban (como indicado por los nombres, por supuesto): un C99-VLA, un tradicional malloc-VLA, un Fixed-LA alocado en el stack, y un Fixed-LA alocado en el heap. Para cada test se utiliza un (gran) array-size de 1000000, y el número de iteraciones se decide arrancando la aplicación (esto es muy útil, como veremos más adelante).

Hay que notar que runTest() utiliza un function pointer para arranar el test (hemos visto algo parecido hablando aquí de las callback): he utilizado la versión extendida de la declaración (void (*funcptr)(int) + llamada de la función con el operador &) pero os recuerdo que, por ejemplo, GCC también digiere fácilmente la declaración simplificada (void funcptr(int) + llamada sin el operador &). La versión extendida es, obviamente, más portátil. Y ya que estamos en el tema de los compiladores: aunque los VLAs (y los loop for (int...)) que utilizo el código de este mes se permiten sólo de C99 en adelante no es necesario (si se utiliza GCC) especificar el flag -std=c99 en compilación: las versiones recientes de GCC incluyen por defecto (al menos) también el C99 (además de las extensiones GNU C): si realmente queréis estar seguros de que lo que habéis escrito cumpla con un standard en particular tenéis que utilizar otros flag: por ejemplo, si queréis escribir utilizando sólo el C89, tenéis que añadir en la línea de compilación: -std=c89 -pedantic. Si estáis utilizando un GCC un poco viejo la compilación del ejemplo os dará warning y/o errores, y habrá que volver a compilar forzando la compatibilidad con el C99.

Los resultados son los siguientes:
aldo@ao-linux-nb:~/blogtest$ gcc vla.c -o vla
aldo@ao-linux-nb:~/blogtest$ ./vla 1000
testVLA       -  Tiempo transcurrido: 4.263985 segundos
testMallocVLA -  Tiempo transcurrido: 3.641929 segundos
testStackFLA  -  Tiempo transcurrido: 4.292963 segundos
testHeapFLA   -  Tiempo transcurrido: 4.285660 segundos
aldo@ao-linux-nb:~/blogtest$ gcc -O2 vla.c -o vla
aldo@ao-linux-nb:~/blogtest$ ./vla 10000
testVLA       -  Tiempo transcurrido: 0.767087 segundos
testMallocVLA -  Tiempo transcurrido: 0.690925 segundos
testStackFLA  -  Tiempo transcurrido: 0.678178 segundos
testHeapFLA   -  Tiempo transcurrido: 0.687785 segundos
Como se puede ver he ejecutado dos test con/sin optimización (flag GCC -O2) y, obviamente, ha sido muy útil el parámetro n-iteraciones de la aplicación, que me ha permitido encontrar un valor adapto para obtener resultados significativos y, al mismo tiempo, para evitar tiempos de ejecución bíblicos con la versión sin optimizaciones. ¿Cómo podemos comentar? Pues bien, ¡el VLA se porta muy bien con/sin optimizaciones! Y consigue, prácticamente, los mismos resultados de su competidor directo, el malloc-VLA, ¡y es más fácil de usar!

Así que, volviendo al tema principal: ¡VLA aprobado!

PERO...

bueno, el porque del VLA malo os lo explicaré en el próximo post, y que sepáis que no todo lo que brilla es oro... y solo por hacer un pequeño spoiler sobre las consideraciones finales: ¡yo nunca utilizo los VLAs en el código que escribo!

¡Hasta el próximo post!

martes, 25 de abril de 2017

The FileCopy
cómo escribir una función de File Copy en C - pt.2

Ok, re-empezamos de donde nos dejamos en el último post (¿lo habeis leido verdad?) Y, como prometido, en esta ocasión el tema será una versión con buffered I/O de la función cpFile(). Es deber, antes, proporcionar otra imagen de The Thing, si no, de lo contrario, podría parecer que esto es sólo un blog de programación, mientras que, como bien sabéis, es un blog para programadores cinefilos...
...con un sombrero así se programa mejor...
Vale, repetimos: I/O buferizado (y por lo tanto, por ejemplo, fread(3) en lugar de read(2), porque el objetivo esta vez es la portabilidad ¿y que hay más portátil (en C) que usar el contenido de stdio.h? El código que veremos en un momento utiliza (casi) el mismo main() de la versión unbuffered y los únicos cambios son internos a la función cpFile(). De hecho, incluso la cpFile() es casi idéntica desde un punto de vista lógico, ya que las funciones buffered tienen una sintaxis de uso y un funcionamiento muy similar a las equivalentes versiones unbuffered. Y claro, si la versión buffered os sale muy diferente (visualmente y lógicamente) de la versión unbuffered es que hay algo que no funciona... bueno, sobre este punto voy a hacer un pequeña digresión al final del post. Por ahora: ¡vamos con el código!
#include <stdio.h>
#include <stdlib.h>

// prototipos locales
static int cpFile(const char* src, const char* dest);

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

    // ejecuta copia
    int retval;
    if ((retval = cpFile(argv[2], argv[1])) < 0) {
        // enseña error y sale con error
        fprintf(stderr, "%s: error: %d\n", argv[0], retval);
        exit(EXIT_FAILURE);
    }

    // sale con Ok
    return EXIT_SUCCESS;
}

// función cpFile()
static int cpFile(
    const char *dest,               // file destinación
    const char *src)                // file fuente
{
    // abre el file fuente
    FILE *fp_in;
    if ((fp_in = fopen(src, "r")) == NULL) {
        // return con error
        return -1;
    }

    // abre el file destinación
    FILE *fp_out;
    if ((fp_out = fopen(dest, "w")) == NULL) {
        // cierra el file y return con error
        fclose(fp_in);
        return -2;
    }

    // r/w loop para la copia usando buffered I/O
    size_t n_read;
    char buffer[BUFSIZ];
    while ((n_read = fread(buffer, 1, sizeof(buffer), fp_in)) > 0) {
        if (! ferror(fp_in)) {
            // write buffer
            fwrite(buffer, 1, n_read, fp_out);
            if (ferror(fp_out)) {
                // cierra los file y return con error
                fclose(fp_in);
                fclose(fp_out);
                return -3;
            }
        }
        else {
            // cierra los file y return con error
            fclose(fp_in);
            fclose(fp_out);
            return -4;
        }
    }

    // cierra los file
    fclose(fp_in);
    fclose(fp_out);

    // return con Ok
    return 0;
}
Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciones y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales. El main(), como se había anticipado, es prácticamente idéntico, mientras que en la cpFile() se repiten, exactamente, las operaciones de la version unbuffered, pero usando fopen(3) en lugar de  open(2), fclose(3) en lugar de close(2), etc. ¿Donde encontramos algunas (pequeñas) diferencias? Sólo en el test de (eventuales) errores de lectura/escritura, que en la versión unbuffered estaban implícitas en las operaciones de read/write (probando si el resultado era igual a -1), mientras que en este caso hay que usar una función a parte, ferror(3), y esto es debido a que:
    On  success,  fread()  and  fwrite() return the number of items read or
    written.  This number equals the number of bytes transferred only  when
    size  is 1.  If an error occurs, or the end of the file is reached, the
    return value is a short item count (or zero).
    fread() does not distinguish between end-of-file and error, and callers
    must use feof(3) and ferror(3) to determine which occurred.
Lo anterior es lo que trae la man-page de fread(3)/fwrite(3), y creo que es una justificación suficiente de por qué he escrito el código asì (y, por lo mismo, no podemos usar strerror(3) en el main() para enseñar los errores). Así que, como se había anticipado, las versiones buffered y unbuffered deben ser casi sobreponibles, y creo que es exactamente el resultado conseguido.

Y ahora la digresión prometida: me ha pasado de encontrar en la red (incluso en apreciados blogs/webs de programación) ejemplos de buffered-copy de file que utilizan unos loop de este tipo:
while (!feof(fp_in)) {
    // lee y escribe buffer
    ...
}
vale, ¿cómo se puede comentar esto? Con una sola palabra:

NO

Si uno escribe el loop de esta manera significa que no ha leído la man-page de fread(3)/fwrite(3) o que la ha leido y no ha entendido el contenido. No hay necesidad de reinventar la rueda, repito: fread(3)/fwrite(3) funcionan casi de la misma manera de read(2)/write(2), por lo que, si el ejemplo de cpFile() unbuffered del post anterior era bueno (¡y lo era!), entonces el ejemplo del post actual debería ser (casi) idéntico. El loop con un while() que testea feof(3) es sintácticamente correcto, pero no lo es lógicamente, ya que comienza testeando algo que todavía no es utilizable (uhmm, ¿una prueba predictiva?) y que, además, no hay necesidad de testear. Bah, no quiero extender demasiado este argumento y os remito a la excelente análisis contenida ahí (en el siempre optimo stackoverflow.com).
Obviamente espero no haber ofendido a nadie (con la digresión anterior): Recordar que errare humanum est... y, por supuesto, también en este blog habré escrito en el pasado algunas tonterías (espero no graves como aquella enseñada hace un momento). Os aseguro, sin embargo, que yo siempre soy muy cuidadoso de no proponer soluciones que no he tenido tiempo para escribir y probar de manera curada, o de lo contrario en lugar de un blog de programación artística este sería un blog de programación a la esperamos que funciona...

¡Hasta el próximo post!