Вы находитесь на странице: 1из 10

Arquitectura de Computadores II (24995)

LABORATORIO
Optimización de Algoritmos para
Aumentar el Rendimiento

Paralelización a Nivel de Instrucción

Alejandro Morales Acedo


NIU: 2139993
METODOLOGÍA GENERAL

1) Analizar el problema que trata de resolver, el algoritmo, y la implementación del programa


que se quiere optimizar:

El algoritmo elegido para realizar la practica es el método de ordenación de la burbuja.

Análisis del algoritmo

Funciona revisando cada elemento de la lista que va a ser ordenada con el siguiente, intercambiándolos
de posición si están en el orden equivocado. Es necesario revisar varias veces toda la lista hasta que no
se necesiten más intercambios, lo cual significa que la lista está ordenada.

Este algoritmo obtiene su nombre de la forma con la que suben por la lista los elementos durante los
intercambios, como si fueran pequeñas "burbujas". También es conocido como el método del
intercambio directo.

Dado que solo usa comparaciones para operar elementos, se lo considera un algoritmo de comparación,
siendo el más sencillo de implementar.

Este algoritmo pasa por todos los elementos de la lista aún estando ordenada, esto supone que sea muy
ineficiente. La complejidad en el mejor de los casos es de Ω(n²).
2) Identificar el núcleo básico del algoritmo, aquél que consume la mayor parte del tiempo de
ejecución. Debe tratarse de una pequeña porción de código, entre 10 y 100 líneas de código:

for (i=0; i<TAM; i++){


for (j=0 ; j<TAM - 1; j++){
if (a[j] > a[j+1]){
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}

3) Contar de forma teórica el número de operaciones básicas de cada tipo (lecturas en memoria,
escrituras en memoria, operaciones de cada tipo con valores enteros, operaciones de cada tipo en
punto flotante, etc.) que hay que realizar dentro de este núcleo básico del algoritmo:

if (a[j] > a[j+1]) -> CMP

temp = a[j]; -> MEM


a[j] = a[j+1]; -> MEM
a[j+1] = temp; -> MEM

j++ -> INT


i++ -> INT

El programa esta compuesto de dos bucles, recorremos el vector tantas veces como posiciones
contenga. Por tanto si tenemos un vector de n posiciones:

– 1er bucle -> n veces


– 2o bucle -> n = 25 veces
– operaciones MEM -> n^2 *3 = 75 operaciones de tipo MEM
– operaciones int -> n^2 + n = 30 operaciones de tipo INT
– operaciones CMP -> n^2 operaciones de tipo CMP

4) Identificar de forma teórica (analizando el código) el patrón de accesos a memoria (el orden de
las direcciones de memoria que el programa va accediendo):

Este programa esta compuesto por dos bucles, estos recorren el vector comparando cada posición con
la siguiente, intercambiándolas en caso de cumplirse la comparación.

El vector lo almacenamos desde la posición 0 hasta la n-1.

Cada iteración del primer bucle supone recorrer completamente el vector.


5) Instrumentar el programa para:

a. Proporcionar datos de entrada al programa y verificar que el programa funciona


correctamente (y que las modificaciones hechas para mejorar el rendimiento no han alterado la
funcionalidad del programa original):

Para realizar las pruebas declaro este vector:

int vector[20]= {54,7,2,33,9,12,2,3,1,26,4,67,87,34,22,97,5,29,54,123};

b. Medir el tiempo de ejecución del programa.

Para realizar las mediciones de tiempo he seguido este método:

struct timeval inici, fi; // declaración de variables

gettimeofday(&inici,NULL); //Aquí empezamos a contar tiempo, lo pongo al iniciar el bucle

// BUCLE PRINCIPAL A ANALIZAR

gettimeofday(&fi,NULL); // cojemos el tiempo al acabar el bucle


printf("\n\nTemps de calcul es: %f segons\n",fi.tv_sec -inici.tv_sec + (fi.tv_usec -
inici.tv_usec)* (1.e-6));

// el tiempo total empleado es la resta entre el tiempo final y el inicial.

c. Y además: contar el nº de operaciones ejecutadas de cierto tipo, mostrar la traza de direcciones


de acceso a memoria (o una parte de ella), …

Tras comprobar el algoritmo con el vector propuesto se realizarán 63 intercambios lo que conlleva
63*3 accesos a memoria adicionales.
La operación INT ( i++) del bucle externo se realizara n veces.
La operación INT ( j++) del bucle interno se realizara n*n veces.

El vector quedará almacenado en memoria de la siguiente forma:

54 7 2 33 9 12 2 3 1 26 4 67 87 34 22 97 5 29 54 123
PARALELISMO a nivel de INSTRUCCIÓN

El método de la burbuja es muy ineficiente, he aplicado dos cambios que mejorarán notablemente el
rendimiento:

• Cada iteración hay comparaciones de elementos consigo mismos, esto nos supone tiempo ya
que si tenemos un vector con 10000 elementos el programa realizará 10000 comparaciones
extra que se pueden simplificar.
• En vez de recorrer por parejas el vector en cada iteración del bucle externo, se puede modificar
y que coja la primera posición del vector comparándola con los elementos que tiene a su
izquierda que son los que ya están ordenados.

El algoritmo modificado teniendo en cuenta lo comentado anteriormente quedaría así:


for (i=0; i<TAM; i++){
for (j=0; j<i; j++){
if (vector[j] > a[i]){
temp = a[j];
a[j] = a[i];
a[i] = temp;
}
}
}
1) Analizar dependencias de datos entre operaciones dentro de una iteración del bucle interno del
programa, y dependencias entre iteraciones consecutivas.

La secuencia ordenada de las instrucciones de una iteración de bucle es:

1 if a [j] > a[j+1] GOTO 5


2 temp = a[j]
3 a[j] = a [j+1]
4 a [j+1] = temp
5 j++
6 if j < TAM GOTO 1
7 i++
8 if i < TAM GOTO 1

La dependencia de datos quedaría así:

En el esquema podemos apreciar las dependencias de datos en una iteración, como podemos ver las tres
operaciones de memoria al hacer el intercambio deben ir seguidas porque dependen de la anterior. En
el instante en el que comparamos j=TAM? , si no ha llegado al valor de TAM se vuelve a empezar la
misma iteración desde la comparación a[j] > a[j+1] hasta la comparación j=TAM?.

En la mejora que he realizado se reduce el número de iteraciones ya que nos ahorramos comparar los
elementos ya ordenados, y comparar un elemento consigo mismo.

Tiempo de ejecución sin mejora: 0.000010 segundos


Tiempo con la mejora: 0.000007 segundos
2) Analizar posibles dependencias de control dentro del bucle.

Las dependencias de control aparecen cuando la ejecución de la instrucciones i++ y j++ dependen de la
instrucción de salto condicional del bucle interno:

Porque la ejecución o no de i++ j++ depende de si el bucle interno finalmente salta o no salta

En nuestro bucle existen 3 dependencias de control:

1 if a[j] < a[j+1] GOTO 5


2 temp = a[j]
3 a[j] = a[j+1]
4 a[j+1] = temp
5 j++
6 if j < TAM GOTO 1
7 i++
8 if i < TAM GOTO 1

En la comparación de la linea 1 si se cumple saltaremos a la linea 5 sino continuaremos por la siguiente


linea para realizar el intercambio.

La segunda dependencia la tenemos en la linea 6, si se cumple ejecutaremos otra vez las lineas 1-6 sino
iremos a la 7.

Por último la tercera estará en la 8, si se cumple volveremos a ejecutar las lineas 1-6 sino acabamos el
bucle.

En el algoritmo mejorado las dependencias de control siguen siendo las mismas.

3) Considerar la posibilidad y el beneficio de intercambiar el bucle interno con uno


de los bucles más externos (loop interchange).

En este caso no conseguiremos nada porque los 2 bucles tienen el mismo recorrido, cambiar bucle
externo por el interno supone cambiar también las variables mas internas del bucle, en ese caso no
ocurre nada por lo dicho anteriormente.
4) Compilar el programa con las opciones de optimización –O2, –O3 y –O4. Observar el código
generado y tomar ideas.

Algoritmo sin mejora:

-02 Loop unrolling mínimo y trace scheduling (opción por defecto)


Total de ciclos -> 102354
Operaciones ejecutadas -> 45100
Numero total de saltos incondicionales -> 2924
Numero total de saltos incondicionales que saltan -> 2262
Fallos de caché de instrucciones -> 408
Fallos de cache de datos -> 218
Instrucciones con 1 operación -> 45,30%
Instrucciones con 2 operación -> 24,07%
Instrucciones con 3 operación -> 7,156%
Instrucciones con 4 operación -> 6,049%

-03 => Loop unrolling básico y trace scheduling


Total de ciclos -> 102392
Operaciones ejecutadas -> 45040
Numero total de saltos incondicionales -> 2904
Numero total de saltos incondicionales que saltan -> 2262
Fallos de caché de instrucciones -> 409
Fallos de cache de datos -> 218
Instrucciones con 1 operación -> 45,73%
Instrucciones con 2 operación -> 23,55%
Instrucciones con 3 operación -> 7,409%
Instrucciones con 4 operación -> 5,992%

-04 => Loop unrolling avanzado


Total de ciclos -> 102392
Operaciones ejecutadas -> 45040
Numero total de saltos incondicionales -> 2904
Numero total de saltos incondicionales que saltan -> 2262
Fallos de cach← de instrucciones -> 409
Fallos de cache de datos -> 218
Instrucciones con 1 operaci -> 45,73%
Instrucciones con 2 operaci -> 23,55%
Instrucciones con 3 operaci -> 7,409%
Instrucciones con 4 operaci -> 5,992%
Algoritmo mejorado:

-02 => Loop unrolling mínimo y trace scheduling (opción por defecto)
Total de ciclos -> 99661
Operaciones ejecutadas -> 43545
Numero total de saltos incondicionales -> 2703
Numero total de saltos incondicionales que saltan -> 2074
Fallos de caché de instrucciones -> 410
Fallos de cache de datos -> 214
Instrucciones con 1 operación -> 44,79%
Instrucciones con 2 operación -> 24,91%
Instrucciones con 3 operación -> 7,882%
Instrucciones con 4 operación -> 1911%

-03 => Loop unrolling básico y trace scheduling


Total de ciclos -> 100332
Operaciones ejecutadas -> 43734
Numero total de saltos incondicionales -> 2685
Numero total de saltos incondicionales que saltan -> 2078
Fallos de caché de instrucciones -> 415
Fallos de cache de datos -> 215
Instrucciones con 1 operación -> 45,13%
Instrucciones con 2 operación -> 24,06%
Instrucciones con 3 operación -> 8,148%
Instrucciones con 4 operación -> 6,423%

-04 => Loop unrolling avanzado


Total de ciclos -> 100332
Operaciones ejecutadas -> 43734
Numero total de saltos incondicionales -> 2685
Numero total de saltos incondicionales que saltan -> 2078
Fallos de caché de instrucciones -> 415
Fallos de cache de datos -> 215
Instrucciones con 1 operación -> 45,13%
Instrucciones con 2 operación -> 24,06%
Instrucciones con 3 operación -> 8,148%
Instrucciones con 4 operación -> 6,423%
5) Desenrollar el bucle interno (loop unroll) y estudiar la forma de modificar el orden de las
operaciones para aumentar el paralelismo.

1 if a [j] < a[j+1] GOTO 5


2 temp = a[j]
3 a[j] = a [j+1]
4 a [j+1] = temp
5 if a [j+1] < a[j+2] GOTO 9
6 temp = a[j+1]
7 a[j+1] = a[j+2]
8 a[j+2] = temp
9 j+=2
10 if j < TAM GOTO 1

De esta manera para cada iteración del bucle hacemos 2 pasadas de las anteriores, así conseguimos que
el bucle se haga la mitad de veces.

El loop unrolling con la mejora seria así:

1 if vector [j] < vector[i] GOTO 5


2 temp = vector[j]
3 vector[j] = vector [i]
4 vector [i] = temp
5 if vector [j+1] < vector[i+1] GOTO 9
6 temp = vector[j+1]
7 vector[j+1] = vector [i+1]
8 vector [i+1] = temp
9 j+=2
11 if j < TAM GOTO 1

6) Analizar la conveniencia de aplicar la optimización software pipelining.

S/W pipelining solapa completamente la ejecución de todas las iteraciones, excepto las primeras y las
últimas.

En este algoritmo el bucle interno, que es el que queremos aplicar software pipelining, esta compuesto
por instrucciones muy dependientes las unas de las otras, tras comprobar con el JavaVex que no
contiene muchos NOP, aplicar esta optimización no mejoraría notablemente el rendimiento.

7) Si es posible, convertir dependencias de control en dependencias de datos.

No he encontrado la manera de realizar este apartado, según lo visto en teoría no he podido aplicarlo a
este algoritmo, tal vez porque no es posible.

Вам также может понравиться