Está en la página 1de 26

Universidad Nacional Autónoma de México

Facultad de Ingeniera

Programación Paralela
Problema de las n reinas

Proyecto 2

Grupo: 05

Equipo: 05

Integrantes:

Cardoso Rodrı́guez Francisco Adrián

Genis Cruz Lourdes Victoria

Medina Segura Fernando

Enero 2021
Objetivos
Objetivo general

Que el alumno ponga en práctica los conceptos de la programación paralela a través de la implementación
de un algoritmo paralelo, ası́ mismo desarrolle su capacidad para responder preguntas acerca de un concepto
analizado a profundidad.

Objetivo del equipo

Aplicar los conceptos de paralelismo vistos en las clases prácticas y de teorı́a sobre un problema previa-
mente resuelto de manera serial como lo es el problema de las n reinas. Ası́ mismo familiarizarse con la
aproximación que el lenguaje C tiene con el paralelismo a través de OpenMP.

Introducción
Tradicionalmente desde el surgimiento de la computación el software ha sido escrito en forma serial. En
la programación serial un problema es dividido en grupos discretos de instrucciones donde los grupos eran
ejecutados secuencialmente uno después de otro, todo era ejecutado sobre un solo procesador. Solamente
después de que una instrucción acababa la siguiente podı́a empezar.

Un ejemplo de la vida real serı́a un banco que tiene una fila y solamente un cajero, hasta que uno termina
lo que quiere hacer en el cajero otro no puede usarlo. El problema se complica cuando en vez de tener una
fila, se tienen varias pero se sigue teniendo un solo cajero, haciendo todo más lento.

Aplicando ese ejemplo a la computación se llegó a la conclusión que al ejecutar una sola instrucción se
estaba desperdiciando una buena cantidad de recursos de hardware porque la tecnologı́a seguı́a mejorando
pero la programación serial la frenaba, para muchos problemas se llegaba a un tope causado por la forma
de programar. A medida que los problemas se hacı́an más pesados y voluminosos, también lo hacı́a la
cantidad de tiempo de ejecución de estos.

Regresando al ejemplo, ¿Cómo se puede mejorar la velocidad en que las dos filas tratan de llegar al cajero?
Fácil, abriendo otro cajero completamente independiente que permita que existan dos filas con dos cajeros
trabajando al mismo tiempo. Esto, viéndolo desde una perspectiva de software se llama programación
paralela.

La programación paralela puede ser confundida con la programación concurrente, esta última parte de
la idea de que se pueden tener múltiples procesos corriendo en un solo procesador, estos procesos a su vez
tienen hilos que permiten la división máxima del programa a ejecutar.

Un hilo es una secuencia de instrucciones atómica, o en otras palabras, indivisibles, es lo mı́nimo que
precisa un proceso para desarrollarse, si estos hilos se trataran de dividir aún más el proceso a realizar
perderı́a sentido.
Entonces, un programa concurrente es aquel formado por varios procesos que se ejecutan en un solo
procesador siguiendo una técnica conocida como paralelismo temporal, que no tiene que ver con la progra-
mación paralela en realidad ¿Entonces por qué se le llama ası́? Para el ojo humano es muy probable que no
se pueda distinguir un programa concurrente de uno paralelo, la diferencia principal entre estos dos radica
en que el programa concurrente va dando permisos a los distintos procesos para ocupar el procesador por
periodos cortos de tiempo, dando la idea de que se ejecutan al mismo tiempo cuando no es realmente ası́.
En cambio, la programación paralela necesita de varios procesadores y de procesos independientes para
llevarse a cabo.
Antecedentes
La programación paralela sigue un esquema de paralelismo espacial, no temporal como en la programación
concurrente, y esto se puede entender como la existencia de varios procesadores fı́sicos que trabajan de
forma simultánea y están conectados mediante una red.

Una variable de este tipo de programación es la programación distribuida, que sigue la misma técnica
que la paralela con la diferencia de que la red que une a los procesadores se extiende a distintos puntos
geográficos.

En la programación paralela se tienen varios procesos ejecutándose sobre distintos procesadores, lo que
trae una serie de ventajas y desventajas.

Ventajas

Eficiencia en el procesamiento.

Aumento de velocidad de ejecución.

Posibilidad de realizar programas más complejos.

Hacer más con menos código.

Desventajas

Muchas veces la implementación paralela de algún algoritmo puede resultar innecesariamente com-
plicada y a veces no solamente no se ven cambios, si no que directamente la versión paralela es peor
que lo que se hacı́a de forma serial.

Muchas veces los procesos no son correctamente divididos por lo que no se aprovechan todos los
procesadores (núcleos).

Dificultad en generar la comunicación y sincronización de los procesos.

Limitaciones de hardware o software.

La relación costo-beneficio no siempre es positiva.

Y lo más importante, no todo es paralelizable porque no todo puede trabajar de forma independiente.

Para el correcto funcionamiento del paralelismo es necesario que los procesadores/núcleos compartan
recursos e información, por lo que debe existir alguna forma de comunicación. Estos procesadores pueden
estar fuerte o débilmente acoplados, o sea, pueden estar mejor o peor coordinados, pueden ser más o menos
independientes y pueden comunicarse más o menos información.

Para los sistemas de multiprocesadores debe existir alguna forma de distribución de la memoria, existen
dos, la memoria compartida y la memoria distribuida.
En la memoria compartida los procesadores tienen una pequeña memoria caché para funcionar pero
para ejecutar un programa comparten la memoria entre sı́, o sea, las direcciones de memoria son únicas e
idénticas sin importar que procesador se tenga como perspectiva.

En la memoria distribuida cada procesador tiene su propia memoria con sus propias direcciones de
memoria pero estos se conectan mediante una red con el resto de los procesadores. Esta red se basa en
alguna topologı́a, algunas más eficientes que otras. Además de eso, los procesadores son capaces de reconocer
la diferencia entre su memoria privada y la de algún otro procesador.

Este tipo de memorias tienen sus propias formas de comunicarse, en el caso de la memoria compartida
existen herramientas tales como semáforos, regiones crı́ticas y monitores. Mientras que del lado de la
memoria distribuida existen los canales y las llamadas a procedimientos remotos.

Otros dos conceptos importantes son la granularidad y el balance de carga. La granularidad ayuda a
entender que tan independientes son los procesadores entre sı́, en otras palabras, que tan capaces son
de realizar un trabajo sin necesidad de hacer algún llamado a otro procesador, entender la granularidad
es importante para encontrar la forma más eficiente de dividir un problema y generar que el tiempo
de ejecución de cada conjunto de instrucciones sea el mı́nimo. Una granularidad gruesa implica mucha
independencia y una granularidad fina hace referencia a procesos o procesadores menos independientes,
dando la sensación de una ejecución serial y no paralela. El balance de carga es la manera de repartir
las tareas entre el número de procesadores que se tienen disponibles, este balance puede ser estático
(previamente asignado) o dinámico (asignado durante ejecución).

Tipos de paralelismo

Paralelismo algorı́tmico
Este paralelismo parte de la idea de la existencia de tareas independientes que ejecutan diferentes
secciones de un algoritmo, ignorando el orden preestablecido de las instrucciones.

Paralelismo geométrico
En este caso el paralelismo se aplica para tareas independientes y exactamente iguales, esto no implica
que sea sobre los mismos datos, en otras palabras lo que se desea realizar en cada tarea es lo mismo
pero los datos de entrada y los de salida pueden ser diferentes.

Paralelismo Farm o paralelismo Manager-Workers


El paralelismo Farm es aquel en el que un procesador es el ”manager se dedica a dividir las tareas
2

entre los demás ”workers”, una vez que estos workers terminan su tarea envı́an los resultados al
manager para que se les asigne una nueva tarea.

Paralelismo a nivel de instrucción


Consiste en cambiar el orden de las instrucciones de un programa para posteriormente juntarlas en
grupos pequeños que se ejecutarán de forma paralela sin alterar la salida final del programa. Las
tareas que son independientes se ejecutan distintos núcleos pero si no lo son pertenecen a 3 tipos
distintos de dependencias.

• Dependencia de flujo: Una tarea requiere sı́ o sı́ de datos de otra tarea para avanzar.
• Anti-dependencia: La tarea 1 requiere de algo de la tarea 2.
• Dependencia de salida: Cuando dos tareas usan la misma información a modificar.
Paralelismo a nivel de bit
Se refiere al aumento del tamaño de la cadena de bits con la que el procesador puede trabajar, este
aumento reduce el número de instrucciones que tiene que ejecutar el procesador en variables cuyos
tamaños sean mayores a la longitud de la cadena.

Paralelismo a nivel de ciclo


Si en un ciclo no existe dependencia entre cada iteración estas se pueden realizar de forma paralela
y no se altera el resultado final.

Paralelismo funcional
El paralelismo funcional trata de resolver las dependencias entre conjuntos de instrucciones para
poder hacer más eficiente el trabajo sobre las tareas independientes, porque si se resuelven esas
dependencias puede dar a lugar a tener más tareas independientes haciendo que el paralelismo tenga
una granularidad más gruesa.

Métricas de desempeño

Las métricas de desempeño sirven, como lo dice su nombre, para medir el desempeño de una solución
paralela a algún problema comparándola con su versión secuencial para ayudar a determinar si vale la pena
o no paralelizar, algunos ejemplos de estas métricas son:

Tiempos de procesamiento y de comunicación


Medición del tiempo que tardan una aplicacion paralela en comparación con su versión serial, puede
verse como la suma del tiempo de procesamiento y el tiempo de comunicación entre procesadores.

Speedup
Es la relación entre el tiempo de ejecución de un programa que se ejecuta en un solo procesador sobre
el tiempo de ejecución en n procesadores, esta métrica solamente toma en cuenta aspectos temporales.

Eficiencia
La eficiencia asocia de forma matemática el tiempo que n procesadores realizan un trabajo en com-
paración con un solo procesador, se calcula como el recı́proco de n. Este valor refleja que tan bien se
aprovecha el hardware del sistema.

Fracción serial
Relaciona otras dos métricas de desempeño, el speedup y la fracción serial con el propósito de tomar
factores extras al tiempo, como lo es el hardware.
Diseño de programas paralelos

El paralelismo requiere de la participación activa del programador puesto que es un proceso casi siempre
manual, para muchos es considerado un nivel más complejo de abstracción pues requiere que se piense no
solamente en la solución del problema, si no en como cada parte del problema se comunica y como trabajan
de forma dependiente o independiente. Existen compiladores o pre-procesadores que ayudan a este trabajo,
y pueden trabajar de manera automática o por instrucción del programador, en el caso del lenguaje C se
suele recurrir a la API OpenMP que sirve para mediante escritura de código, convertir bloques seriales a
paralelos, conociendo bien su funcionamiento y del programa en si mismo.

Modelo PCAM

Propuesto por Ian Foster, el modelo PCAM existe para ayudar en la realización de programas de natu-
raleza paralela, este modelo se puede resumir en cuatro aspectos:

Partición
El problema se debe partir de la mejor forma posible en tareas pequeñas, ignorando por el momento
cantidad de procesadores, organización de memoria, sincronización etc. El enfoque va en la división
y en encontrar oportunidades de código paralelizable. La división del problema se conoce como
descomposición, y esta puede ser de dominio o funcional.

Comunicación
Se busca la coordinación de las tareas y como se van a comunicar las distintas partes del programa,
identificando las dependencias. Los tipos de comunicación son: local/global, estructurada, estáti-
ca/dinámica y sı́ncrona/ası́ncrona.

Aglomeración
La aglomeración es el proceso inverso de la partición, es determinar como es que las tareas previamente
divididas se juntarán y cuales no, buscando mantener el sistema sin afectaciones.

Mapeo
Asignar los distintos bloques de tareas a los procesadores o hilos, como estos se distribuirán sobre el
sistema paralelo.
Lenguajes de programación paralelos

Un lenguaje paralelo se caracteriza por tener implementado de alguna forma paralelismo, secuencialidad,
comunicación, sincronización y no determinismo.

El no determinismo hace referencia a que el orden en el que ocurren los eventos en el paralelismo es
completamente arbitrario.

En este caso, que se trabajó con lenguaje C, no es necesario explicar como funciona el paralelismo en
lenguajes con otros paradigmas como el orientado a objetos.
Descripción del algoritmo
El algoritmo seleccionado es el del problema de las n reinas. Este problema fue propuesto por el ajedrecista
alemán Max Bezzel en 1848. La reina es posiblemente la pieza más útil en un juego de ajedrez porque es
una pieza muy poderosa porque puede moverse en todas las direcciones adyacentes posibles (adelante,
atrás, izquierda, derecha y las 4 diagonales posibles) y Max creyó que serı́a interesante encontrar todas las
formas posibles en que un número n de reinas podı́a ser colocado en un tablero de nxn sin que ninguna
reina pudiera atacar a otra, como lo muestra el siguiente ejemplo:

Como se puede observar, ninguna reina (denotadas como Q) puede atacar a otra dada la posición en
la que fueron colocadas. Para encontrar una solución es necesario que dos reinas no se encuentren en la
misma fila, columna o diagonal.

Dependiendo del número n de reinas se pueden tener un número diferente de soluciones, algunas solu-
ciones son simplemente rotaciones de otras soluciones pero no dejan de ser soluciones.

Este problema es más que un simple pasatiempo, tiene aplicaciones en ramas como control de tráfico
aéreo, sistemas de comunicaciones, compresión de datos, procesamiento paralelo óptimo, balance de carga
o ruteo de mensajes, esto porque sus soluciones garantizan que cada objeto puede ser accesado desde sus
direcciones vecinas.

En programación existen varias aproximaciones a la solución de este problema, en este caso se hizo uso
de un arreglo multidimensional que se inicializa en ceros, y cada vez que se encuentra la posición en la
que una reina debe ir el valor de ese ı́ndice cambia a uno. El algoritmo es recursivo debido a que una vez
que se coloca la primera reina se deben revisar las posiciones vecinas para que la siguiente reina ’sepa’ que
no puede ponerse ahı́. Paralelizar este algoritmo requiere de la aplicación de varias directivas de OpenMP
que permitan que ciertos ciclos y tareas independientes se ejecuten sobre más de un procesador, la recur-
sividad puede complicar la paralelización porque en muchas ocasiones una recursión depende de la anterior.

Paralelización del algoritmo


Tipo de paralelismo

El tipo de paralelismo presente en el algoritmo va desde paralelismo a nivel de ciclo (algunos ciclos for
son paralelizables debido a que sus iteraciones no son dependientes) hasta otros tipos de paralelismo como
paralelismo a nivel de instrucción, las tareas independientes se ejecutan en procesadores independientes
mientras que las tareas que no son independientes presentan algún tipo de las dependencias expuestas
en los antecedentes; se puede encontrar dependencia de flujo y dependencia de salida, más que nada al
momento de evaluar la posición en la que debe ir la reina.

Métricas de desempeño

Tiempos de procesamiento y comunicación


El problema de las n reinas tiene distintos tiempo de ejecución, procesamiento y comunicación depen-
diendo del tamaño del problema (cantidad n de reinas). Para efectos de pruebas de duración media
se decidió que la cantidad de reinas sea de 13.
Es importante destacar que el crecimiento de soluciones disponibles en el problema de las n reinas
tiene un crecimiento acelerado.

En tiempos de ejecución, con 13 reinas, y después de 5 pruebas, se tienen los siguientes promedios
en la versión serial contra la versión paralelizada.

• Serial: 7.439104 segundos.


• Paralela: 7.522734 segundos segundos.

Speedup
Recordando que speedup relaciona el tiempo de ejecución sobre n procesadores y el tiempo sobre
un procesador hace falta hacer pruebas tanto en la versión serial como paralela, la versión serial se
ejecuta sobre un procesador y la paralela sobre varios. En este caso se harán varias pruebas para 13
reinas midiendo los tiempos, sacando promedio y dividiendo el tiempo promedio en un procesador
entre el tiempo promedio en n procesadores.
Tomando en cuenta los resultados obtenido en la prueba anterior y aplicando la formula matemática
se tiene que:
Eficiencia
Como ya se mencionó en los antecedentes el objetivo de la eficiencia es tomar en cuenta más factores
además de los temporales, para lograr esto se toma en cuenta el número de procesadores que están
trabajando sobre el programa, en este caso, y debido al uso de la función omp get num threads se
puede saber que se está trabajando con 8, además de que la computadora donde se hicieron estas
mediciones cuenta con el Intel Core i7-2635QM de 4 núcleos y 8 hilos con 6 MB de memoria caché y
más 16 GB de memoria RAM Dual-Channel a 1333 MHz.
Sabiendo eso, y conociendo el valor del Speedup, se sabe que la eficiencia se puede definir como el
Speedup entre el número de procesadores, o sea:

Fracción serial
La fracción serial relaciona el speedup y la eficiencia con la siguiente fórmula matemática:

Y haciendo las sustituciones pertinentes se obtiene que la fracción serial da como resultado: 1.01285

Viendo los resultados arrojados por las métricas de desempeño se puede concluir que al menos esta
implementación de paralelismo sobre el problema de las n reinas resulta no ser una mejor opción que la
versión serial, harı́a falta explorar la idea de una mejora en el algoritmo que pueda hacer que valga la pena
la paralelización, pero por el momento parece más una pérdida de recursos.

Formas de comunicación

La arquitectura de las computadoras personales es generalmente con memoria compartida lo que implica
que se puede llegar a hacer uso de formas de comunicación tales como monitores, semáforos o regiones
crı́ticas. En el código se puede observar la presencia de semáforos cuando alguna región paralela hace uso
de una variable que no es privada. La presencia de regiones crı́ticas se puede ver al momento de querer
imprimir el tablero de ajedrez con las reinas, los procesos se excluyen entre sı́ para garantizar que la
impresión salga correctamente, ası́ mismo al usar el constructor parallel for para llenar de ceros el arreglo
de reinas (que después se irá actualizando según se encuentre la posición en la que debe ir una reina.
Granularidad

Dado el uso de directivas como single en el código se puede observar que existe una granularidad gruesa,
o sea, existen pocas llamadas de comunicación o sincronización, o sea, los hilos están trabajando casi por su
cuenta sin necesidad de acceder constantemente a lo que hacen otros, sin embargo, al tener una arquitectura
de memoria compartida esa caracterı́stica de granularidad gruesa se hace más fina, porque la arquitectura
de memoria compartida tiene generalmente una sobrecarga de comunicación baja, que es la más adecuada
para un funcionamiento con granularidad fina. Para buscar la posición en la que debe ir la reina se divide
el proceso en tareas grandes. Debido a esto, se realiza una gran cantidad de cálculo en los procesadores, lo
que genera un grano grueso resultando en un posible desequilbrio de carga, puede haber procesadores muy
cargados y procesadores inactivos.
Implementación en C con OpenMP

Una vez entendido como funciona el problema de las n reinas se debe tener en cuenta que si bien no
existen tableros que no sean de 64 casillas, es decir de 8 x 8, abstractamente tomamos la idea de n x n
porque un algoritmo no puede funcionar únicamente para un solo valor dado, si no para cualquier valor n
que se le dé; es importante considerar que el algoritmo puede dar de resultado una única solución o todas
las combinaciones posibles dependiendo del número de reinas y el tamaño del tablero, para el algoritmo que
trabajamos, decidimos usar esta segunda forma, esto para considerar una salida que tarde en ejecutarse
para tamaños n mayores a 8, este problema se puede considerar como NP completo porque para n muy
pequeñas se visibiliza bien si la solución dada es correcta mientras que la solución de n muy grandes es
más difı́cil de visibilizar si esa respuesta o todas las respuestas son correctas.
El algoritmo que va a resolver esta problemática esta hecho en la API de OpenMp la cual se implementa
en el lenguaje de programación C, se considera la versión serial también para entender como se diferencia
el tiempo de salida con respecto a la versión paralela, ambas versiones toman soluciones de un tablero de
n x n con n reinas, es decir, si el tablero es de 6 x 6, habrá 6 reinas en el tablero, primero veremos cómo
se realizó la versión secuencial.

Para el algoritmo serial consideramos importante que el tamaño del tablero debe ser dinámico por lo
dicho que el algoritmo plantea soluciones para cualquier tamaño n, asi que decidimos implementar una
matriz de n x n para visualizar de mejor forma los tiempos de ejecución de la salida del programa, la
representación del tablero se hace mediante una matriz. Para resolver este problema creamos 3 funciones
adicionales al a función principal, las cuáles se llaman “colocarReina”, “ImprimirTablero” y “comprobar”.
Primero en la función principal se tiene considerado importante el tiempo de ejecución, para esto se importa
la biblioteca “time.h”, posteriormente se crea la matriz de n x n, donde n es un dato el cual se define como
una constante al inicio del programa, se llena la matriz de ceros, que representan las casillas vacı́as del
tablero, se manda llamar a la función “colocarReina”, después de realizar todo el algoritmo se imprime el
número de soluciones posibles y el tiempo total de ejecución.

La función “colocarReina” es una función que no devuelve valores y recibe como parámetros la matriz
que representará el tablero y el número de columnas donde se iniciará el programa, al ser el algoritmo
secuencial, el número de columna debe ser cero, posteriormente validamos si el número de columna es igual
al del número total de reinas, si se cumple se llama a la función “ImprimirTablero”, y además se cumpla
o no esta condición se entra a un ciclo “for” donde dentro de este ciclo hay una sentencia de control que
manda a llamar a la función “comprobar”, y si se valida que se puede colocar la reina, se sustituye la
posición actual por un uno que indica que se coloca una reina en esa posición del tablero; esto se realiza
ası́ porque se valida primero fila por fila y después columna por columna las posiciones válidas para las
n reinas y posteriormente imprimir todas las combinaciones de soluciones posibles, cabe resaltar que esta
función es recursiva, donde el caso base es cuando el número de columna sea igual al número de reinas el
tablero, y cada llamada recursiva sirve para recorrer las columnas agregándole una unidad a la variable
que representa el número de columna el cuál se ha pasado como parámetro desde la función principal.

La función “comprobar” devuelve un valor entero, cero o uno, cero representa un valor booleano falso
que indica que no se puede colocar la reina en esa posición, mientras que el uno es un valor booleano
verdadero que indica que si se puede colocar a la reina en esa casilla del tablero, la función recibe como
parámetros la matriz, el número de fila actual el cual se indica con el valor del ı́ndice del ciclo for de la
función “colocarReina”, y el número de columna que de igual forma se actualiza cada que finalize una
iteración del ciclo “for” de la función “colocarReina”; esta función utiliza tres ciclos “for” distintos que
ayudan a recorrer las filas y columnas del tablero y validan que las reinas no estén en la misma fila, columna
o diagonal de otra reina, estas validaciones se realizan con sentencias “if” que validan si la variable i que
representa la fila y la variable j que representa a la columna no sean iguales y se pueda colocar a la reina.
La función “ImprimirTablero” es una función que no devuelve valores y recibe como parámetro la matriz
que representa el tablero, esta función con dos ciclos “for”, un ciclo para recorrer las filas y el otro ciclo
para recorrer las columnas del tablero, imprimen los valores de la matriz, al ser ya validadas las posiciones
gracias a las otras funciones, esta función ya no necesita realizar validaciones e imprime directamente cada
casilla del tablero, donde los ceros representan casillas vacı́as y los unos representan a una reina colocada
en esa casilla del tablero.

En general este algoritmo lo que hace es recorrer por filas y por hileras el tablero de tamaño ya definido
previamente por una constante, se valida que las posiciones no estén ocupadas o no estén en el rango de
movimiento de otra reina, se llena la matriz con estos valores y finalmente se imprimen todas las soluciones
posibles para un tablero de n x n con n reinas en el, y también se visualiza el número total de soluciones
y su tiempo de ejecución, este último es el más importante pues se busca que la versión paralela de este
algoritmo sea aun más rápida que esta versión.

Para realizar el algoritmo paralelo es necesario importar la biblioteca “omp.h” para poder utilizar los
constructores para realizar un programa paralelo, el principal constructor es el “pragma omp parallel” el
cual va a definir que región del algoritmo queremos paralelizar, y al región que se busca paralelizar es la
que va parte donde se colocan las reinas, es decir, la función “colocarReina” debe ir dentro de esta región;
a su vez esta función esta dentro de otra región que se define por el cosntructor “single” el cual nos ayuda
a que un solo hilo ejecute esa instrucción y no se mezclen datos, y a su vez la función está dentro de una
región definida por el constructor “taskgroup”, el cuál indica que otro hilo debe esperar a que este finalize
la ejecución de un hilo previo con todas sus tareas respectivas.

Posteriormente dentro de la función “colocarReina” al momento de validar que se ha finalizado el reco-


rrido del tablero, la función que imprime el tablero esta dentro de una región definida por el constructor
“critical”, la cuál se encarga de evitar el “data racing”, es decir, que las soluciones que obtiene cada hilo no
se mezclen y evitar que otro hilo gane el procesador cuando un hilo vaya a dar una solución al problema;
después dentro de esta misma función, al realizar la llamada recursiva, se coloca un constructor llamado
“task firstprivate” y recibe de parámetro una variable, la cual hace que en la primera iteración de un ciclo
se vuelva este valor privado, es decir, no se comparta este dato con otros hilos y se pierdan los resultados,
en este caso esa variable es el número de columna de cada iteración, esto para no saltarse columnas y no
se pierda una posible solución a la problemática.

La versión paralela de este algoritmo ayuda a entender como se puede dividir las tareas y dar con las
soluciones más rápido que en la versión serial, de igual forma se incluye el tiempo de ejecución y el número
de soluciones totales, este tiempo se va a comparar con el tiempo obtenido en la versión secuencial y se
puede obtener ası́ una conclusión general de ambas versiones.
Pruebas de rendimiento
Versión secuencial

Para una mejor visualización nos ayudaremos con la siguiente figura.En donde tenemos que nuestra N=
6.

Figura 1 Soluciones N reinas (N=6).


Como podemos ver existen cuatro soluciones posibles las cuales concuerdan con la ejecución de nuestro
programa secuencial (Figura 1.2).

Figura 1.1 Ejecución N-Reinas secuencial


Figura 1.2 Método colocarReina(int reinas[N][N], int columna)

En la Figura 1.1 El método consiste en dividir el tablero en N columnas distintas, y empezar a colo-
car las reinas en cada columna. Primero, colocar una reina en la primera fila disponible de la columna
actual y pasar a la siguiente columna. Si la columna que se está revisando ya no tiene filas disponibles, es
necesario volver y mover la reina en la columna anterior a la siguiente fila disponible de dicha columna.
Una vez que se encuentra una solución, se agrega a la cuenta, y se continúa el recorrido del árbol. En
la lı́nea 32, tenemos un caso “base” en donde si llegamos a 6 o N= 6, se encontraron las soluciones y
mandamos a imprimir por pantalla. Con el ciclo for(i = 0; i ¡N; i++) lo que se está haciendo es comprobar
si la ubicación de la reina es correcta. Aplicándolo a nuestro ejemplo N=6,una vez que el algoritmo llegue
a seis el programa indicará que la reina no se debe colocar en ese lugar y deberá regresar, avanzar en la fila
anterior uno más y volver a empezar el recorrido hasta encontrar su posición. En otras palabras, con el for
lo que haces es recorrer cada una de las casillas del tablero. En la lı́nea 36, mandamos a llamar a nuestro
método comprobar y pasamos como parámetros (la matriz, la fila y la columna). En caso de cumplir la
condición, la función se manda a llamar ası́ misma.

Figura 1.2 Método comprobar(int reinas[N][N], int fila, int columna)


En la Figura 1.2 debemos comprobar dos condiciones importantes, la primera de ellas “No hay reinas
que comparten la misma columna” y como segunda condición tenemos “No hay reinas que comparten la
misma diagonal”

En la Figura 1.3 se imprime el tablero con las reinas en su respectiva posición.

Pruebas para varias instancias

Debido al tamaño de las soluciones sólo se mostrarán las últimas impresiones de los tableros y el número
de soluciones que corresponden a cada una de las pruebas.
Con N=6
Con N=8

Con N=10
Con N=12
Gráficos

A continuación se muestran los gráficos tiempo ejecución tanto del algoritmo secuencial como el algoritmo
paralelo.
En el gráfico Secuencial y Paralelo se puede ver una pequeña diferencia en cuanto a los tiempos de ejecu-
ción en donde la ventaja la lleva el algoritmo secuencial. En primera instancia este algoritmo tiene mucho
paralelismo implı́cito, dado que si analizamos cada proceso o hilo puede trabajar de forma independiente,
es decir, cada hilo busca la posición correcta en donde se deberı́a colocar la reina. En otras palabras, el
algoritmo es altamente paralelizable.

Sin embargo existe un detalle que complica la paralelización y es que cuando resolvemos el programa de
manera secuencial utilizamos tres variables ( que guardan las filas, diagonales izquierdas y las diagonales
derechas) dichas variables guardan una copia mientras que en la versión se guardan varias de estas copias
no solo una. En conclusión este algoritmo no es apto para ser paralelizable.
Conclusiones
Cardoso Rodrı́guez Francisco Adrián:

El desarrollo del proyecto requirió la resolución de un problema que mis compañeros ya habı́an tenido
la suerte de trabajar en el pasado, por lo que la comprensión del mismo se facilitó bastante. Fue necesario
investigar acerca de directivas que no se vieron en el temario sin embargo haber revisado las directivas
básicas de OpenMP en clase sirvió como parteaguas para un mejor entendimiento de lo que se encontrara
en la red, como la directiva firstprivate. La abstracción para la implementación paralela requirió de un
esfuerzo colectivo porque visualizar el trabajo que los hilos tenı́an que realizar y como unos eran depen-
dientes de otros fue muy complicado. Tan complicado que considero que la versión proporcionada no es la
implementación más óptima. Se llegó al resultado de que la versión paralela era en la mitad de los casos,
peor que la serial (por los tiempos y velocidades de comunicación), ası́ que considero que nuestro trabajo
es funcional, pero es mejorable.

Genis Cruz Lourdes Victoria:

A pesar de los problemas que tuvimos a la hora de la realización del código en paralelo, se logró terminar a
tiempo el proyecto. Además aplicamos los conceptos de programación paralela a través de la implementación
del problema N-Reinas, para ello nos basamos en la versión secuencial de dicho algoritmo. Además en
nuestro caso en particular retomamos los conceptos vistos en las prácticas OpenMP. Asimismo observamos
que nuestro código se compone por dos tipos de paralelismo (a nivel de ciclo y a nivel de instrucción).
De igual forma, mediante la utilización de las métricas de desempeño, las pruebas realizadas y los gráficos
evaluamos el rendimiento de nuestro algoritmo paralelo con respecto a su contraparte secuencial. En cuanto
a lo abordado con anterioridad, se llegó a la conclusión de que a pesar de que nuestro algoritmo diera indicios
de ser altamente paralelizable y tener ventaja con respecto al serial, resultó no ser del todo cierto ya que
analizando los tiempos de ejecución el secuencial ganó por un pequeño rango, sin embargo analizando todo
lo que conlleva paralelizar el código, mientras que el secuencial es más rápido de implementar, se concluye
que el algoritmo no es apto para ser paralelizable.

Medina Segura Fernando:

El problema de las n reinas nos hizo ver una forma de implementar algoritmos paralelos, aunque por las
caracterı́sticas de nuestro algoritmo no se pudo llevar a cabo una paralelización de calidad y por consiguiente
los tiempos de ejecución del programa paralelo resultan ser mayores con respecto a la versión serial, si bien
esta diferencia de tiempos es muy mı́nima, a grandes rasgos resulta un desperdicio de recursos y por eso es
que este problema en particular no es conveniente volverse un algoritmo paralelizado. En cuanto a nuestras
habilidades de programación, el uso de OpenMP nos ayudo a entender de mejor forma como funcionan
los algoritmos paralelos en la programación estructurada en el lenguaje C, entendiendo de mejor forma
conceptos como las métricas de desempeño y la granularidad y que independientemente de si el algoritmo
paralelo haya resultado eficiente o no, debemos tomar en cuenta que lo importante es entender como se
puede volver un algoritmo secuencial un algoritmo paralelo y eso lo consideramos lo más importante en el
desarrollo de nuestro proyecto.
Autoevaluación general del equipo
Nuestro trabajo y esfuerzo ha sido sobresaliente, aunque consideramos que podemos mejorar. Lastimo-
samente, tuvimos algunos problemas a la hora de implementar el código en paralelo, sin embargo nos
sentimos satisfechos con el resultado final y sacamos al máximo el provecho de la asignatura. En cuanto
al trabajo en equipo todos los integrantes del equipo se expresaron de manera clara y aceptamos positiva-
mente los distintos puntos de vista. El aporte del material (documentación y códigos) ofrecen un enfoque
real a la materia. Además nos ha parecido una idea interesante la creación de un código paralelo mediante
su versión secuencial ya que otorga dos puntos de vista distintos de la programación. Cabe destacar que
para la realización de este proyecto se utilizaron los conceptos vistos en nuestras últimas clases teóricas ası́
como de las prácticas 11-13. En términos generales dada la calidad del trabajo y el esfuerzo de todos los
integrantes consideramos que un 8 de calificación podrı́a considerarse una buena calificación.

Bibliografı́a

https://docs.microsoft.com/en-us/cpp/parallel/openmp/reference/openmp-functions?view=
msvc-160

https://docs.microsoft.com/en-us/cpp/parallel/openmp/reference/openmp-directives?view=
msvc-160

https://hpc.llnl.gov/training/tutorials/introduction-parallel-computing-tutorial

https://www.geeksforgeeks.org/introduction-to-parallel-computing/

https://ferestrepoca.github.io/paradigmas-de-programacion/paralela/paralela_teoria/index.
html

También podría gustarte