programas.
Facultad de Inform´atica
Departamento de Sistemas Inform´aticos y Computaci´on Universidad Polit´ecnica de Valencia
Curso 2002/2003
1. Introducci´ on
El objetivo de esta pr´actica es aprender a calcular experimentalmente el coste de un algoritmo. Para ello, se proponen dos m´etodos de medida:
mediante el conteo de operaciones significativas, o mediante un reloj. La caracterizaci´on de un algoritmo mediante la definici´on de su coste computa- cional (tanto en espacio necesario en memoria como en tiempo de CPU) es una tarea importante en cualquier ´area de la programaci´on de aplicaciones, siendo cr´ıtica en entornos donde la memoria es limitada (tarjetas inteli- gentes) 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´alidas. Sin embargo, unas soluciones pueden ser mejores que otras. Un algoritmo se dice que es mejor que otro para una aplicaci´on determinada, si su coste espacial (memoria necesaria) o su coste temporal (tiempo de CPU) es menor que el segundo.
2. Coste de un algoritmo
2.1. Coste espacial
El coste espacial de un algoritmo es la cantidad de memoria que va a necesitar para su ejecuci´on.
Supongamos el siguiente ejemplo. Se desea calcular la media de 10000 n´umeros enteros que se encuentran en un fichero. Dos alumnos proponen las siguientes soluciones:
1. Definir un vector de enteros de tama˜no 10000, leer todo el fichero dentro del vector, y calcular la media del vector.
1
del fichero. Una vez que se haya obtenido la suma, se calcula la media dividiendo el contenido del acumulador por 10000.
¿Qu´e soluci´on crees que es m´as eficiente respecto al coste espacial?
2.2. Coste temporal
El coste temporal de un algoritmo indica la cantidad de tiempo de proce- so que se necesita para resolver un problema. Dicho coste se puede expresar de varias formas, por ejemplo: n´umero de veces que se ejecuta un bucle, n´umero de operaciones significativas ejecutadas (acceso a un elemento de un vector, una operaci´on matem´atica, etc) o cantidad de tiempo consumido.
La ventaja de las dos primeras formas de calcular el coste de un algoritmo es que son v´alidas tanto te´orica (se puede calcular el n´umero de pasos que va a dar un bucle sin necesidad de utilizar el ordenador) como experimen- talmente (se puede incluir c´odigo en el programa para que lleve la cuenta del n´umero de veces que se pasa por una cierta instrucci´on). Sin embargo, la utilizaci´on del tiempo para caracterizar un algoritmo es muy dependiente de la m´aquina y del momento de ejecuci´on del programa, por lo que s´olo es v´alida para medidas experimentales.
Ejercicio. Dado el programa siguiente, modif´ıcalo para que lleve la cuen- ta del n´umero de veces que se ha ejecutado la instrucci´on del bucle m´as interno. Antes de terminar, deber´a mostrar por pantalla dicho n´umero.
#include <stdio.h>
int main() { int i,j,n,t;
printf("\nIntroduce un n´umero: ");
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´agina 2 de 15
flop. Un flop, o floating point operation se define como el esfuerzo com- putacional necesario para efectuar una operaci´on en la que intervienen n´umeros 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 al- goritmo
Para calcular experimentalmente la eficiencia de un algoritmo es necesa- rio seguir los siguientes pasos:
1. Implementar el algoritmo en un lenguaje de programaci´on adecuado.
2. Generar un conjunto de pruebas que muestren los distintos comporta- mientos del algoritmo (en el caso de que el coste del algoritmo var´ıe en funci´on de los datos de entrada).
3. Resolver con el algoritmo dichos conjuntos de prueba, aumentando la talla del problema. Para cada ejecuci´on, 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´a que generar distintos casos que muestren dichas variacio- nes. Si se desea estudiar el comportamiento de un algoritmo determinado, como por ejemplo la b´usqueda secuencial de un elemento dentro de un vec- tor, se deben estudiar los siguientes casos:
Caso peor. Se busca aquella configuraci´on de los datos de entrada que hace que el algoritmo se comporte peor. En el ejemplo de la b´usqueda 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´on ne- cesita 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´as interesante, ya que es el que, es- tad´ısticamente, se acercar´a al caso promedio, y el que definir´a el com- portamiento del algoritmo en la mayor parte de las ocasiones. Este
caso se mide generando aleatoriamente instancias del problema. Da- do que aleatoriamente se puede generar el caso peor o el caso mejor, habr´a 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´umeros enteros siempre cuesta lo mismo, independientemente de los valores a sumar. En estas ocasiones, no hay distinci´on entre los casos mejor, peor o promedio.
Azar determinista. Es posible hacer que el ordenador genere n´umeros enteros pseudoaleatorios mediante las siguientes funciones, definidas en stdlib.h:
int random(void);
void srandom(unsigned int semilla);
La funci´on random devuelve un n´umero entero entre 0 y la constante RAND_MAX (2147483647 en las m´aquinas del laboratorio). Cada vez que se invoca devuelve un nuevo n´umero. Dicho n´umero se calcula mediante una funci´on matem´atica que depende del valor generado anteriormente. El valor que define el comienzo de una serie de valores pseudoaleatorios se denomina semilla. La funci´on srandom1permite establecer dicha semilla. A partir de una semilla dada, se generar´a siempre la misma serie de n´umeros.
Es com´un necesitar n´umeros menores que RAND_MAX. Para convertir los va- lores devueltos por random a un rango menor, se puede utilizar el operador m´odulo (%). Por ejemplo, para obtener n´umeros entre 0 y 100, se puede utilizar:
a=random()%101;
Ejercicio. Escribe un programa que escriba en pantalla 10 n´umeros alea- torios entre 1 y 10.
1En 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
Repetitivo. Ejecuta varias veces el programa anterior. ¿Qu´e observas en los resultados? ¿A qu´e crees que es debido?
Para establecer una semilla del generador de n´umeros aleatorios distinta en cada ejecuci´on, se suele utilizar el reloj del sistema. As´ı, es muy probable que dos ejecuciones del programa generen series de n´umeros distintas. La funci´on time de la librer´ıa time.h devuelve el n´umero de segundos trans- curridos desde el 1 de enero de 1970. A continuaci´on se muestra un ejemplo de utilizaci´on de esta funci´on 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´on de cada uno de ellos.
Ejercicio. Completa el siguiente programa, que calcula el n´umero 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´umero de comparaciones realizadas */
...
/* Imprimir en una l´ınea por pantalla: tam coste */
printf("%d\t%d\n",tam,cont);
}
return 0;
}
Al mostrar el resultado del ejercicio anterior mediante una gr´afica, se obtendr´a un resultado parecido al mostrado en la Figura 1.
0 5000 10000 15000 20000 25000 30000 35000
0 50000 100000 150000 200000 250000
Coste de la b´usqueda
Comparaciones
Talla
Figura 1: Gr´afica de un posible resultado del programa de la p´agina 5
Ejercicio. Observando la gr´afica anterior, ¿crees que el resultado es correc- to? ¿Muestra la gr´afica el comportamiento promedio de la b´usqueda se- cuencial de elementos en un vector? Modifica el programa para calcular una aproximaci´on al comportamiento promedio de dicho algoritmo.
3.3. Presentar los resultados adecuadamente
La salida del programa anterior son dos columnas de n´umeros, que a primera vista puede ser dif´ıcil de interpretar. El uso de gr´aficas como la mostrada en la Figura 1 facilita la interpretaci´on de los resultados. A con- tinuaci´on se presenta una herramienta que permite la creaci´on de dichas gr´aficas de una forma sencilla, a partir de datos formateados.
3.3.1. Dibujo de gr´aficas con gnuplot
gnuplot es un dibujador de gr´aficas 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:
2Se puede descargar desde la p´agina web http://www.gnuplot.info
[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´aticas con el comando plot.
Por ejemplo, para dibujar la funci´on 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 fun- ciones definidas por gnuplot.
Dado un fichero de texto con el siguiente formato:
# Tiempo Pasos
10000 5004
20000 9852
30000 15327
...
230000 115328 240000 118646 250000 127508
gnuplot puede dibujar cada l´ınea del archivo como un punto, donde el primer n´umero 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´acter # se ignoran. La instrucci´on para dibujar dicha gr´afica es la siguiente:
Figura 2: Gr´afica de la funci´on seno
gnuplot> plot ’resbusca2.txt’
donde resbusca2.txt es el nombre del fichero que se encuentra en el direc- torio actual y contiene la informaci´on 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´afica de puntos, donde cada l´ınea del archivo indicado se con- vierte 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´as columnas en el fichero, se ignoran. Los l´ımites de los ejes mostrados en la gr´afica se ajustan a los datos de entrada. En la parte superior derecha de la gr´afica se muestra la leyenda de la gr´afica, donde se muestra un punto exactamente igual a los utilizados en la gr´afica, 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´on> | ’fichero_datos’ [using <cols>]}
[title ’Titulo’] [with <estilo>] [, <otra funci´on o fichero>]
donde:
[<rangos>]: Tama˜no de los ejes X e Y. Por ejemplo:
plot [0:20] [-1:1] sin(x)
<funci´on>: Especifica la funci´on a dibujar
’fichero_datos’: nombre del fichero con los datos a dibujar.
Figura 3: Dibujo de una gr´afica 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´a 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´on se muestra una tabla con otras instrucciones comunes de GNUPLOT:
Comando Acci´on
help Muestra la ayuda
set xlabel ’Etiqueta’ Etiqueta del eje X set ylabel ’Etiqueta’ Etiqueta del eje Y
set title ’T´ıtulo’ T´ıtulo principal del gr´afico cd <directorio> Cambia el directorio actual
quit Terminar
Ejercicio. Dibuja el resultado de la modificaci´on del ejercicio propuesto en la p´agina 6. Utiliza l´ıneas para dibujarlo y llama a la curva Promedio b´usqueda. El eje X deber´a mostrar la etiqueta Talla, y el eje Y Compara- ciones. Para volcar la salida por pantalla de un programa a un fichero de texto, se puede utilizar la redirecci´on de la salida est´andar, 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´afica que muestra el comportamiento de un algoritmo, es necesario encontrar la funci´on matem´atica que describa de forma m´as precisa el comportamiento de dicho algoritmo. gnuplot propor- ciona el comando fit para ajustar una funci´on dada por el usuario a unos puntos definidos en un archivo. La sintaxis de dicho comando es:
fit <funci´on> ’fichero_datos’ [using <cols>] via <var1> [,<var2>...]
donde:
<funci´on>: es la funci´on a ajustar. Se debe haber definido previamente [using <cols>]: indica el orden en las que se utilizar´an las columnas del fichero
via <var1>[,<var2>...]: especifica los par´ametros de la funci´on a ajustar.
Por ejemplo, el fichero datos.txt define la curva mostrada en la Figu- ra 4.
Por inspecci´on de la curva, parece que los puntos siguen un comporta- miento cuadr´atico. As´ı, hay que definir un polinomio cuadr´atico gen´erico, 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 nor- males de C, adem´as del operador **, que indica exponenciaci´on.
La funci´on f(x) no es directamente representable porque las variables a, by c no tienen valor definido. Para darles aquel valor que haga que la funci´on f(x) se ajuste lo m´as 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´afica:
0 500 1000 1500 2000 2500 3000
0 5 10 15 20 25 30 35 40 45 50
’datos.txt’
Figura 4: Gr´afica generada a partir de unos puntos de entrada.
gnuplot> plot ’datos.txt’ title ’Puntos’ w l, f(x) tit ’Funci´on’
y la Figura 5 muestra el resultado.
La selecci´on de la familia de funciones que se utilizar´a para ajustar los puntos encontrados experimentalmente se puede basar en dos m´etodos, que dependen si el c´odigo fuente del programa que gener´o los puntos est´a dispo- nible o no.
Si el c´odigo fuente est´a disponible, se puede calcular el coste del mismo.
Para ello hay que buscar la zona de c´odigo que consume mayor tiempo de computaci´on. Normalmente dicha zona est´a localizada en uno o m´as bucles del programa, que se ejecutar´an m´as o menos veces dependiendo de la talla del problema. De la inspecci´on de dichos bucles, se debe poder extraer el coste esperado (ver el primer ejercicio de los Ejercicios propuestos).
Si el c´odigo fuente de la funci´on que se desea estudiar no est´a disponible, entonces la familia de funciones se deber´a derivar de la observaci´on de los puntos que describen el tiempo de ejecuci´on del algoritmo, en funci´on de la talla. En este caso, se deber´a utilizar un reloj para medir el tiempo de ejecuci´on para cada talla del problema. La siguiente secci´on explica c´omo utilizar el reloj del sistema.
Ejercicio. Ajusta los puntos obtenidos en el ejercicio de la p´agina 6 a la funci´on matem´atica que estimes conveniente mediante el comando fit de gnuplot.
0 500 1000 1500 2000 2500 3000
0 5 10 15 20 25 30 35 40 45 50
Puntos Funci´on
Figura 5: Ajuste de los puntos de la Figura 4 mediante una funci´on
3.4. Medida de tiempos de ejecuci´on
En los compiladores ANSI C est´andar se puede encontrar la funci´on clockdefinida en time.h:
clock_t clock(void);
La funci´on clock devuelve una aproximaci´on 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´en definida en time.h.
Para calcular el tiempo que ha tardado el ordenador en ejecutar un bloque de c´odigo, se puede utilizar el siguiente m´etodo:
int t1,t2;
double resultado;
...
t1=clock();
/* C´odigo a medir */
t2=clock();
resultado=((double)(t2-t1))/CLOCKS_PER_SEC;
Ejercicio. Calcula experimentalmente el coste de las operaciones suma y producto de matrices, y ajusta los resultados obtenidos a las fun- ciones matem´aticas que estimes oportunas. Utiliza para ello los fiche- ros matrix.h y matrix.c que se encuentran en el directorio /misc/
practicas/asignaturas/prgfi/p6. Tu programa deber´a mostrar por pantalla tres columnas, con la talla de la matriz, el tiempo que ha ne- cesitado una suma y el tiempo que ha necesitado un producto:
# Talla tsuma tprod
10 0.0003 0.009
20 0.0012 0.072
30 0.0027 0.243
...
100 0.03 9
Te puedes basar en el ejercicio de la p´agina 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 relle- nar 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
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´un ordenador que pueda sumar dos matrices en tiempo cero). El problema es la precisi´on del reloj.
Las unidades de la funci´on clock son demasiado grandes para medir los tiempos de ejecuci´on de las operaciones. La soluci´on a este problema es repetir la operaci´on un n´umero 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´on, habr´a que dividir por el n´umero de veces que se ha repetido dicha operaci´on.
Ejercicio. Modifica el ejercicio anterior para evitar que aparezcan tiempos nulos.
4. Ejercicios propuestos
1. A continuaci´on se muestran los fragmentos de programas que se han detectado como los que consumen m´as tiempo de ejecuci´on. A partir del c´odigo 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.
for (i=0;i<n;i++) ...
for (i=0;i<10;i++) for (j=0;j<n;j++) ...
for (i=0;i<n;i++) for (j=0;j<10;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++) {
printf("%d",i);
for (j=n;j>0;j--) for (k=0;k<n-10;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´an implementadas las funciones f1, f2, f3 y f4. No se dispone del c´odigo fuente de dichas funciones, pero se desea caracte- rizar 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´ametro de tipo entero, que es el que deter- minar´a el tiempo de ejecuci´on 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´on mediante la funci´on ma- tem´atica que estimes oportuna.