PRACTICA 6. Cálculo de complejidad de programas.

Anuncio
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
Descargar