...hola soy un VLA: comienza a preocuparte... |
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 segundosComo 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!
No hay comentarios:
Publicar un comentario