Programación (PRG) PRACTICA 6. Cálculo de complejidad de programas. Facultad de Informática Departamento de Sistemas Informáticos y Computación Universidad Politécnica de Valencia Curso 2002/2003 1. Introducción El objetivo de esta práctica es aprender a calcular experimentalmente el coste de un algoritmo. Para ello, se proponen dos métodos de medida: mediante el conteo de operaciones significativas, o mediante un reloj. La caracterización de un algoritmo mediante la definición de su coste computacional (tanto en espacio necesario en memoria como en tiempo de CPU) es una tarea importante en cualquier área de la programación de aplicaciones, siendo crı́tica en entornos donde la memoria es limitada (tarjetas inteligentes) o la velocidad de respuesta debe cumplir unos requisitos mı́nimos (sistemas de respuesta en tiempo real). En general, todo problema se puede resolver de varias formas, todas ellas válidas. Sin embargo, unas soluciones pueden ser mejores que otras. Un algoritmo se dice que es mejor que otro para una aplicación determinada, si su coste espacial (memoria necesaria) o su coste temporal (tiempo de CPU) es menor que el segundo. 2. 2.1. Coste de un algoritmo Coste espacial El coste espacial de un algoritmo es la cantidad de memoria que va a necesitar para su ejecución. Supongamos el siguiente ejemplo. Se desea calcular la media de 10000 números enteros que se encuentran en un fichero. Dos alumnos proponen las siguientes soluciones: 1. Definir un vector de enteros de tamaño 10000, leer todo el fichero dentro del vector, y calcular la media del vector. 1 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas 2. Definir un acumulador donde se va sumando cada entero que se lee del fichero. Una vez que se haya obtenido la suma, se calcula la media dividiendo el contenido del acumulador por 10000. ¿Qué solución crees que es más eficiente respecto al coste espacial? 2.2. Coste temporal El coste temporal de un algoritmo indica la cantidad de tiempo de proceso que se necesita para resolver un problema. Dicho coste se puede expresar de varias formas, por ejemplo: número de veces que se ejecuta un bucle, número de operaciones significativas ejecutadas (acceso a un elemento de un vector, una operación matemática, etc) o cantidad de tiempo consumido. La ventaja de las dos primeras formas de calcular el coste de un algoritmo es que son válidas tanto teórica (se puede calcular el número de pasos que va a dar un bucle sin necesidad de utilizar el ordenador) como experimentalmente (se puede incluir código en el programa para que lleve la cuenta del número de veces que se pasa por una cierta instrucción). Sin embargo, la utilización del tiempo para caracterizar un algoritmo es muy dependiente de la máquina y del momento de ejecución del programa, por lo que sólo es válida para medidas experimentales. Ejercicio. Dado el programa siguiente, modifı́calo para que lleve la cuenta del número de veces que se ha ejecutado la instrucción del bucle más interno. Antes de terminar, deberá mostrar por pantalla dicho número. #include <stdio.h> int main() { int i,j,n,t; printf("\nIntroduce un número: "); scanf("%d",&n); i=0;t=0; while (i<n) { for (j=0;j<n;j++) t+=3; i+=2; } printf("\nt=%d\n",t); return 0; } 10 de diciembre de 2002 Página 2 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas flop. Un flop, o floating point operation se define como el esfuerzo computacional necesario para efectuar una operación en la que intervienen números reales. Una medida muy extendida para comparar la velocidad de distintos computadores son los MFLOPS (leı́do mega-flops), o millones de operaciones en coma flotante por segundo que pueden ejecutar. 3. Estudio experimental de la eficiencia de un algoritmo Para calcular experimentalmente la eficiencia de un algoritmo es necesario seguir los siguientes pasos: 1. Implementar el algoritmo en un lenguaje de programación adecuado. 2. Generar un conjunto de pruebas que muestren los distintos comportamientos del algoritmo (en el caso de que el coste del algoritmo varı́e en función de los datos de entrada). 3. Resolver con el algoritmo dichos conjuntos de prueba, aumentando la talla del problema. Para cada ejecución, generar una medida del esfuerzo que se ha invertido. 4. Presentar los resultados adecuadamente. 3.1. Generar conjuntos de prueba En el caso de que el comportamiento del algoritmo dependa de los datos de entrada, habrá que generar distintos casos que muestren dichas variaciones. Si se desea estudiar el comportamiento de un algoritmo determinado, como por ejemplo la búsqueda secuencial de un elemento dentro de un vector, se deben estudiar los siguientes casos: Caso peor. Se busca aquella configuración de los datos de entrada que hace que el algoritmo se comporte peor. En el ejemplo de la búsqueda secuencial, el caso peor se da cuando se busca un elemento que no se encuentra en el vector (hay que recorrer todos sus elementos). Caso mejor. Es aquel conjunto de datos de entrada cuya solución necesita el mı́nimo esfuerzo. En el ejemplo, es el caso cuando el primer elemento del vector es el elemento buscado. Caso promedio. Este caso es el más interesante, ya que es el que, estadı́sticamente, se acercará al caso promedio, y el que definirá el comportamiento del algoritmo en la mayor parte de las ocasiones. Este 10 de diciembre de 2002 Página 3 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas caso se mide generando aleatoriamente instancias del problema. Dado que aleatoriamente se puede generar el caso peor o el caso mejor, habrá que repetir varias veces el experimento, para poder calcular la media de dichos experimentos. Hay casos en los que el coste del algoritmo no depende de los datos de entrada. Por ejemplo, la suma de dos vectores de N números enteros siempre cuesta lo mismo, independientemente de los valores a sumar. En estas ocasiones, no hay distinción entre los casos mejor, peor o promedio. Azar determinista. Es posible hacer que el ordenador genere números enteros pseudoaleatorios mediante las siguientes funciones, definidas en stdlib.h: int random(void); void srandom(unsigned int semilla); La función random devuelve un número entero entre 0 y la constante RAND_MAX (2147483647 en las máquinas del laboratorio). Cada vez que se invoca devuelve un nuevo número. Dicho número se calcula mediante una función matemática que depende del valor generado anteriormente. El valor que define el comienzo de una serie de valores pseudoaleatorios se denomina semilla. La función srandom1permite establecer dicha semilla. A partir de una semilla dada, se generará siempre la misma serie de números. Es común necesitar números menores que RAND_MAX. Para convertir los valores devueltos por random a un rango menor, se puede utilizar el operador módulo (%). Por ejemplo, para obtener números entre 0 y 100, se puede utilizar: a=random()%101; Ejercicio. Escribe un programa que escriba en pantalla 10 números aleatorios entre 1 y 10. 1 En Windows estas funciones se llaman rand y srand. Para utilizar siempre random y srandom y compilar el mismo programa en Windows y en Linux puedes poner en la cabecera del programa: #ifndef random #define random rand #define srandom srand #endif 10 de diciembre de 2002 Página 4 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas Repetitivo. Ejecuta varias veces el programa anterior. ¿Qué observas en los resultados? ¿A qué crees que es debido? Para establecer una semilla del generador de números aleatorios distinta en cada ejecución, se suele utilizar el reloj del sistema. Ası́, es muy probable que dos ejecuciones del programa generen series de números distintas. La función time de la librerı́a time.h devuelve el número de segundos transcurridos desde el 1 de enero de 1970. A continuación se muestra un ejemplo de utilización de esta función para establecer la semilla. srandom(time(NULL)); 3.2. Aplicar los casos de prueba al algoritmo Este paso consiste en resolver cada uno de los casos de prueba generados, calculando el coste de resolución de cada uno de ellos. Ejercicio. Completa el siguiente programa, que calcula el número medio de pasos necesarios para buscar un elemento dentro de un vector: #include <stdio.h> #include <stdlib.h> #define MAX 250000 int main(void) { int i,tam,x,cont; int v[MAX]; /* Inicializar v con valores entre 1 y MAX */ for (i=0;i<MAX;i++) v[i]=i+1; /* Para tam = {10000, 20000, 30000 ... MAX} */ for (tam=... { /* x es un entero aleatorio entre 1 y tam */ x=... /* Buscar x dentro de v. Calcular el número de comparaciones realizadas */ ... /* Imprimir en una lı́nea por pantalla: tam printf("%d\t%d\n",tam,cont); coste */ } return 0; } 10 de diciembre de 2002 Página 5 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas Al mostrar el resultado del ejercicio anterior mediante una gráfica, se obtendrá un resultado parecido al mostrado en la Figura 1. Coste de la búsqueda 35000 30000 Comparaciones 25000 20000 15000 10000 5000 0 0 50000 100000 150000 200000 250000 Talla Figura 1: Gráfica de un posible resultado del programa de la página 5 Ejercicio. Observando la gráfica anterior, ¿crees que el resultado es correcto? ¿Muestra la gráfica el comportamiento promedio de la búsqueda secuencial de elementos en un vector? Modifica el programa para calcular una aproximación al comportamiento promedio de dicho algoritmo. 3.3. Presentar los resultados adecuadamente La salida del programa anterior son dos columnas de números, que a primera vista puede ser difı́cil de interpretar. El uso de gráficas como la mostrada en la Figura 1 facilita la interpretación de los resultados. A continuación se presenta una herramienta que permite la creación de dichas gráficas de una forma sencilla, a partir de datos formateados. 3.3.1. Dibujo de gráficas con gnuplot gnuplot es un dibujador de gráficas interactivo. Es un programa que se distribuye bajo licencia GNU, y hay versiones disponibles para Linux, Windows y otros sistemas operativos 2 . Para ejecutarlo, se debe lanzar el comando gnuplot desde un terminal: 2 Se puede descargar desde la página web http://www.gnuplot.info 10 de diciembre de 2002 Página 6 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas [fjabad@pc0101 p6]$ gnuplot G N U P L O T Linux version 3.7 patchlevel 1 last modified Fri Oct 22 18:00:00 BST 1999 Copyright(C) 1986 - 1993, 1998, 1999 Thomas Williams, Colin Kelley and many others Type ‘help‘ to access the on-line reference manual The gnuplot FAQ is available from <http://www.ucc.ie/gnuplot/gnuplot-faq.html> Send comments and requests for help to <[email protected]> Send bugs, suggestions and mods to <[email protected]> Terminal type set to ’unknown’ gnuplot> gnuplot permite dibujar funciones matemáticas con el comando plot. Por ejemplo, para dibujar la función seno se utiliza: gnuplot> plot sin(x) El resultado de la orden anterior se puede ver en la Figura 2. Mediante la orden help functions se puede consultar la lista de funciones definidas por gnuplot. Dado un fichero de texto con el siguiente formato: # Tiempo 10000 20000 30000 ... 230000 240000 250000 Pasos 5004 9852 15327 115328 118646 127508 gnuplot puede dibujar cada lı́nea del archivo como un punto, donde el primer número es la coordenada en el eje X, y el segundo es la coordenada en el eje Y. Las lı́neas que empiezan con el carácter # se ignoran. La instrucción para dibujar dicha gráfica es la siguiente: 10 de diciembre de 2002 Página 7 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas Figura 2: Gráfica de la función seno gnuplot> plot ’resbusca2.txt’ donde resbusca2.txt es el nombre del fichero que se encuentra en el directorio actual y contiene la información a dibujar. El resultado se puede ver en la Figura 3. Por defecto, cuando se utiliza el comando plot como se acaba de ver, genera una gráfica de puntos, donde cada lı́nea del archivo indicado se convierte en un punto. La primera columna dentro del archivo de texto es la coordenada en el eje X, y la segunda columna la coordenada en el eje Y. Si hay más columnas en el fichero, se ignoran. Los lı́mites de los ejes mostrados en la gráfica se ajustan a los datos de entrada. En la parte superior derecha de la gráfica se muestra la leyenda de la gráfica, donde se muestra un punto exactamente igual a los utilizados en la gráfica, junto al nombre del fichero. Sin embargo, plot es muy potente, y admite gran variedad de opciones. La sintaxis de dicho comando es: plot [rangos] {<función> | ’fichero_datos’ [using <cols>]} [title ’Titulo’] [with <estilo>] [, <otra función o fichero>] donde: [<rangos>]: Tamaño de los ejes X e Y. Por ejemplo: plot [0:20] [-1:1] sin(x) <función>: Especifica la función a dibujar ’fichero_datos’: nombre del fichero con los datos a dibujar. 10 de diciembre de 2002 Página 8 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas Figura 3: Dibujo de una gráfica mediante puntos [using <cols>]: especifica el orden de las columnas que se van a utilizar como ejes X e Y. Por ejemplo: plot ’datos.txt’ u 3:1 [title ’Titulo’]: define el tı́tulo de la curva que aparecerá en la leyenda [with <estilo>]: estilo puede ser: points, lines, linespoints, impulses. . . Por ejemplo: plot x w points, x**2 with lines plot sin(x) with impulses A continuación se muestra una tabla con otras instrucciones comunes de GNUPLOT: Comando help set xlabel ’Etiqueta’ set ylabel ’Etiqueta’ set title ’Tı́tulo’ cd <directorio> quit 10 de diciembre de 2002 Acción Muestra la ayuda Etiqueta del eje X Etiqueta del eje Y Tı́tulo principal del gráfico Cambia el directorio actual Terminar Página 9 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas Ejercicio. Dibuja el resultado de la modificación del ejercicio propuesto en la página 6. Utiliza lı́neas para dibujarlo y llama a la curva Promedio búsqueda. El eje X deberá mostrar la etiqueta Talla, y el eje Y Comparaciones. Para volcar la salida por pantalla de un programa a un fichero de texto, se puede utilizar la redirección de la salida estándar, mediante el sı́mbolo >. Por ejemplo: resbusca2 > resultado.txt. 3.3.2. Ajuste de funciones con gnuplot Una vez que se ha obtenido la gráfica que muestra el comportamiento de un algoritmo, es necesario encontrar la función matemática que describa de forma más precisa el comportamiento de dicho algoritmo. gnuplot proporciona el comando fit para ajustar una función dada por el usuario a unos puntos definidos en un archivo. La sintaxis de dicho comando es: fit <función> ’fichero_datos’ [using <cols>] via <var1> [,<var2>...] donde: <función>: es la función a ajustar. Se debe haber definido previamente [using <cols>]: indica el orden en las que se utilizarán las columnas del fichero via <var1>[,<var2>...]: especifica los parámetros de la función a ajustar. Por ejemplo, el fichero datos.txt define la curva mostrada en la Figura 4. Por inspección de la curva, parece que los puntos siguen un comportamiento cuadrático. Ası́, hay que definir un polinomio cuadrático genérico, para posteriormente ajustarlo. Para ello, se ejecuta la orden: gnuplot> f(x)=a*x**2+b*x+c Dentro de gnuplot se pueden definir funciones con los operadores normales de C, además del operador **, que indica exponenciación. La función f(x) no es directamente representable porque las variables a, b y c no tienen valor definido. Para darles aquel valor que haga que la función f(x) se ajuste lo más posible a los puntos anteriores, se puede utilizar el comando fit: gnuplot> fit f(x) ’datos.txt’ via a,b,c El siguiente comando muestra ambas curvas en la misma gráfica: 10 de diciembre de 2002 Página 10 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas 3000 ’datos.txt’ 2500 2000 1500 1000 500 0 0 5 10 15 20 25 30 35 40 45 50 Figura 4: Gráfica generada a partir de unos puntos de entrada. gnuplot> plot ’datos.txt’ title ’Puntos’ w l, f(x) tit ’Función’ y la Figura 5 muestra el resultado. La selección de la familia de funciones que se utilizará para ajustar los puntos encontrados experimentalmente se puede basar en dos métodos, que dependen si el código fuente del programa que generó los puntos está disponible o no. Si el código fuente está disponible, se puede calcular el coste del mismo. Para ello hay que buscar la zona de código que consume mayor tiempo de computación. Normalmente dicha zona está localizada en uno o más bucles del programa, que se ejecutarán más o menos veces dependiendo de la talla del problema. De la inspección de dichos bucles, se debe poder extraer el coste esperado (ver el primer ejercicio de los Ejercicios propuestos). Si el código fuente de la función que se desea estudiar no está disponible, entonces la familia de funciones se deberá derivar de la observación de los puntos que describen el tiempo de ejecución del algoritmo, en función de la talla. En este caso, se deberá utilizar un reloj para medir el tiempo de ejecución para cada talla del problema. La siguiente sección explica cómo utilizar el reloj del sistema. Ejercicio. Ajusta los puntos obtenidos en el ejercicio de la página 6 a la función matemática que estimes conveniente mediante el comando fit de gnuplot. 10 de diciembre de 2002 Página 11 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas 3000 Puntos Función 2500 2000 1500 1000 500 0 0 5 10 15 20 25 30 35 40 45 50 Figura 5: Ajuste de los puntos de la Figura 4 mediante una función 3.4. Medida de tiempos de ejecución En los compiladores ANSI C estándar se puede encontrar la función clock definida en time.h: clock_t clock(void); La función clock devuelve una aproximación del tiempo de procesador consumido por el programa. Las unidades en las que devuelve dicho tiempo son unidades de reloj, y para convertirlas en segundos hay que dividir por la constante CLOCKS_PER_SEC, también definida en time.h. Para calcular el tiempo que ha tardado el ordenador en ejecutar un bloque de código, se puede utilizar el siguiente método: int t1,t2; double resultado; ... t1=clock(); /* Código a medir */ t2=clock(); resultado=((double)(t2-t1))/CLOCKS_PER_SEC; 10 de diciembre de 2002 Página 12 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas Ejercicio. Calcula experimentalmente el coste de las operaciones suma y producto de matrices, y ajusta los resultados obtenidos a las funciones matemáticas que estimes oportunas. Utiliza para ello los ficheros matrix.h y matrix.c que se encuentran en el directorio /misc/ practicas/asignaturas/prgfi/p6. Tu programa deberá mostrar por pantalla tres columnas, con la talla de la matriz, el tiempo que ha necesitado una suma y el tiempo que ha necesitado un producto: # Talla 10 20 30 ... 100 tsuma 0.0003 0.0012 0.0027 tprod 0.009 0.072 0.243 0.03 9 Te puedes basar en el ejercicio de la página 5 para estructurar tu programa, y lo estudiado en el Apartado 3.4 para medir los tiempos de las operaciones. No tienes que implementar las operaciones sobre matrices (ni tampoco debes modificar los ficheros matrix.h o matrix.c). Para utilizar las funciones de matrix.c en otro fichero: 1. Incluir la cabecera matrix.h en el programa donde se vayan a usar sus funciones. 2. Llamar a las funciones normalmente. Tienes funciones para rellenar una matriz con valores (iniciaM), para mostrarla por pantalla (escribeM), para sumar dos matrices (sumaM) y para multiplicarlas (productoM). 3. Para compilar el programa, utilizar: gcc -o prog prog.c matrix.c 10 de diciembre de 2002 Página 13 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas Tiempo cero. Es posible que, al ejecutar el programa anterior, obtengas resultados parecidos a estos: [fjabad@pc0101 p6]$ midematrix 10 0.000000 0.000000 20 0.000000 0.000000 30 0.000000 0.000000 40 0.000000 0.000010 50 0.000000 0.000000 60 0.000000 0.000020 70 0.000000 0.000010 80 0.000000 0.000030 90 0.000000 0.000040 100 0.000000 0.000060 Evidentemente, este resultado es falso (no hay ningún ordenador que pueda sumar dos matrices en tiempo cero). El problema es la precisión del reloj. Las unidades de la función clock son demasiado grandes para medir los tiempos de ejecución de las operaciones. La solución a este problema es repetir la operación un número de veces suficiente como para que el tiempo sea significativo. Luego, a la hora de sacar la cantidad de segundos que ha tardado en realizarse una operación, habrá que dividir por el número de veces que se ha repetido dicha operación. Ejercicio. Modifica el ejercicio anterior para evitar que aparezcan tiempos nulos. 4. Ejercicios propuestos 1. A continuación se muestran los fragmentos de programas que se han detectado como los que consumen más tiempo de ejecución. A partir del código de los bucles, indicar el coste esperado de cada ejemplo, para una talla de problema n, y la familia de funciones que se deberı́a utilizar para ajustar su comportamiento. 10 de diciembre de 2002 Página 14 de 15 Prácticas PRG. Facultad de Informática DSIC Curso 2002/2003 PRACTICA 6. Cálculo de complejidad de programas for (i=0;i<n;i++) ... for (i=0;i<n;i++) for (j=0;j<10;j++) ... for (i=0;i<n;i++) { printf("%d",i); for (j=n;j>0;j--) for (k=0;k<n-10;k++) ... } for (i=0;i<10;i++) for (j=0;j<n;j++) ... for (i=0;i<n;i++) for (j=0;j<n;j++) for (k=0;k<n;k++) ... for (i=0;i<n;i++) for (j=0;j<n;j++) acum=acum+A[i][j]; for (k=0;k<n;k++) acum=acum-k; 2. En el fichero enigma.o del directorio /misc/practicas/asignaturas/ prgfi/p6, están implementadas las funciones f1, f2, f3 y f4. No se dispone del código fuente de dichas funciones, pero se desea caracterizar su comportamiento temporal. Las cabeceras de las funciones se encuentran en el fichero enigma.h, dentro del mismo directorio. Todas las funciones reciben un parámetro de tipo entero, que es el que determinará el tiempo de ejecución de cada una de ellas. Para compilar el programa que utilice dichas funciones, utilizar: gcc -o prog prog.c enigma.o -lm Ajusta el comportamiento de cada función mediante la función matemática que estimes oportuna. 10 de diciembre de 2002 Página 15 de 15