Dise˜no, Implementación y Evaluación de un procesador multi

Anuncio
Universidad de Buenos Aires
Facultad de Ingenierı́a
Tesis de Grado de Ingenierı́a Electrónica
Diseño, Implementación y
Evaluación de un procesador
multi-núcleo
Alumno: Sr. Federico Giordano Zacchigna
Director: Dr. Ing. Ariel Lutenberg
31 de julio de 2012
i
A mi familia
ii
iii
Resumen y motivación del presente trabajo
En este trabajo se presenta una implementación de un procesador multinúcleo. El mismo esta basado en el procesador Plasma, que es un procesador de
código libre, simple que ha sido utilizado para la realización de varios proyectos
y pruebas, y que esta basado en la arquitectura de instrucciones MIPS. El
procesador ha sido objeto de estudio en varios trabajos, entre los que se cuentan
trabajos realizados por integrantes de nuestro grupo en dónde se estudia su
funcionamiento bajo efectos de radiación e interferencia electromagnética. En la
actualidad se desea continuar con esta lı́nea de investigación sobre procesadores
multi-núcleos y por eso surge la necesidad de implementar este procesador. Las
caracterı́sticas antes nombradas sobre el procesador Plasma, son ideales para
la realización de este trabajo. El sistema plasma multi-núcleo tiene distintas
aplicaciones directas, entre ellas:
Sistemas tolerantes a fallas utilizables en ambientes de alta interferencia
electromagnética y expuestos a radiación no ionizante, para aplicaciones
en sistemas de seguridad de reactores nucleares y sistemas de control y
navegación de satélites y vehı́culos espaciales. Este tema es de especial
interés hoy en dı́a para instituciones como CONAE, CNEA e INVAP.
Investigaciones cientı́ficas relacionadas con el estudio de los efectos de la
radiación y la interferencia electromagnética sobre los FPGA’s donde se
utilice una versión funcional de un softcore que sirva para realizar mediciones y sacar conclusiones sobre los efectos que estos fenómenos tienen
sobre los dispositivos.
Desarrollos de sistemas multi-núcleos parametrizables mixtos (Asymetric
Multicore Architecture) para aplicaciones de alto rendimiento, donde se
implementan micros óptimos para distintos procesos, por ejemplo, ejecución de RTOS, procesamiento de señales, GPU’s, comunicaciones, etc.
Un objetivo importante en la implementación del procesador multi-núcleo
que se realiza en este trabajo es que sea de forma tal que se puedan instanciar
un número genérico de núcleos durante el proceso de sı́ntesis, y las partes más
relevante del diseño de la arquitectura del procesador son la arquitectura de la
memoria, la memoria cache y la comunicación entre los distintos núcleos. A lo
largo del trabajo se detallan los principales factores que influyen sobre el diseño
del mismo. También se muestran las principales complicaciones que aparecen
y las soluciones a las mismas. Se muestran los detalles de su implementación
en VHDL. Se realizan pruebas del mismo en un kit Nexys2 de Digilent, basado
en una FPGA Xilinx Spartan3E-1200. Finalmente se presentan los resultados
obtenidos.
iv
v
Agradecimientos
En primer lugar, agradezco al Laboratorio de Sitemas Embebidos de la Universidad de Buenos Aires, lugar en el cual fue llevado a cabo el presente trabajo.
Agradezco a su director y mi tutor, el Dr. Ing. Ariel Lutenberg, por el apoyo
brindado. Agradezco también a Lucas Chiesa por la ayuda que me brindó al
realizar el trabajo.
Agradezco especialmente a los miembros del jurado, el Ing. Juan Manuel
Cruz, el Ing. Nicolás Alvarez, y el Ing. Fabián Vargas miembro de la Pontifı́cia
Universidade do Rio Grande do Sul (PUCRS), por ceder parte de su tiempo
para evaluar el presente trabajo.
Parte del desarrollo de este trabajo de tesis tuvo lugar en el Institut für Datentechnik und Kommunikationsnetze (IDA) de la Universidad Técnica Braunschweig. Agradezco a su director Peter Rüffer, y a mis compañeros del laboratorio Mark y Mustafa. Agradezco al DAAD y al Ministerio de Educación
de la República Argentina por brindarme esta posibilidad, al darme una beca.
Agradezco a los integrantes del programa ALE-ARG del DAAD, Agustin Rosembaum, Alejando Rodriguez, Eliseo Rocchetti, Federico Beltzer, Lucas Claramonte, Mauro Calabria, Ariel Malawka, Sofı́a Carolina Visintini. Agradezco
a todas las otras personas que formaron parte de mi vida mientras estuve en
Alemania, Jose Alejandro Diaz Vides, Ismael Holgueras de Lucas, Dani Umpierrez Corona, Mariana Almeida Ribeiro, Audrey Segura Medina, Valerio Roger
Lasso, José Rivero Rodrı́guez, Maria y Angela Cildoz Guembe. Y agradezco especialmente a aquellos con quienes logre tener una afinidad especial durante mi
estadı́a en Alemania, personas que me ayudaron mucho con su compañia y apoyo en los momentos difı́ciles, Sergio Medina, con quien compartı́ un tiempo en
Parı́s, Mariel Figueroa, Bruno Strappa, Bruno Emmanuel Rossi, Mari Antber,
Ana Rodriguez, Juan José Baena Castillo, Belen Kistner, Miguel Mamani.
Durante los primeros cinco años y medio de carrera tuve el agrado de conocer a muchas personas con quienes trabajé en MAN Ferrostaal, a todos ellos
quiero agradecerles, Leandro Feniello, Olga Hiczuk, Alicia Hiczuk, Martin Kent,
Eduardo Kenda, Daniel Morales, Sergio Acri, Walter Allaltune, Osvaldo Preiti,
Lucas De La Canal, Marta, Romina Lepore, Daniela Islas y al resto de los integrantes.
Agradezco a todos los profesores que formaron parte de mi formación académica, a aquellos de la facultad y a aquellos del mi colegio, el Instituto Hölter Schule,
pero especialmente a mis profesores de electrónica a quienes aprecio mucho, Ing.
Rubén Saclier, Ing. Norberto Muiño, Ing. Carlos Siganotto y al Ing. Charly, y
a mis profesores de alemán especialmente a Diana, Geraldine Lorenzo, Fedor
Pellmann y muy de corazón a Juana Dartsch, quien fue por lejos mi mejor
profesora.
Agradezco a las personas que forman parte de la materia Dispositivos Semiconductores, de la cual formo parte hace ya más tres años, Mariano Garcı́a
Inza, Sebastián Carbonetto, Diego Martin, Claudio Pose, Luciano César Natale,
Gabriel Sanca y en su momento Daniel Rus.
Agradezco a todos mis compañeros de la facultad de ingenierı́a entre ellos,
Fernando Chouza, Diego Martı́n, Fabricio Alcalde, Gabriel Gabian, Pablo Delgado, Enzo Lanzelotti, Fernando Berjano, Paola Pezoimburu, Federico Roasio,
Ezequiel Espósito, Claudio Pose, Andres Manikis y a todos aquellos que no
recuerde en este momento. También agradezco a aquellos que dejaron ser com-
vi
pañeros de la facultad, para pasar a ser amigos, Luciano César Natale, Germán
Acosta, Diego Vilaseca, Gustavo Dı́az, Matı́as Weber, Claudio Lupi, Manuel
Fernández, Lucas Sambuco,
Quiero agradecer a todas las personas importantes que pasaron por mi vida
durante este perı́odo, ya que no sólo estoy terminando mi carrera sino culminando una gran etapa en mi vida, y todos estas personas formaron parte de ella,
Lis Weiss, Jose Spillmann, Marcelo Razeto, Sandra Canepuccia, Santiago Razeto, Natalia y Carla Belén Rossi, Sebastián Salles, Alejandro sansalone, Glenda
Busch, Sarah Collins, Ana Belén Garcı́a, Barbara Beutel, Ignacio Unrrein, Lucas
Sakalis, Mauro Garcı́a Alena, Alejandro Mauch, Sebastián Deter, Gaston Santana, Pablo Falcioni, Martı́n Burgueño, Alejandro Sappracone, Hernán Goncalves.
Agradezco de también a mis más grandes amigos, a quienes siempre voy
a querer, quienes fueron compañeros en buenos y malos momentos, quienes
siempre estuvieron, con quienes compartir dı́as y noches de estudio, salidas e
infinidad de cosas y por eso son especiales para mi, Jesica Lazart, Mariana Ende,
Barbará y Daniela Spillmann Weiss, Agustina Cánepa, Franco Gallardi, Hernán
Pedros, Juan Sist, Patricio Helfrich, Leonardo Unger, Mauricio Koller, Fernando
Perez, Matias Schwabauer e Ignacio Razeto.
No puedo dejar de agradecer a la familia Hoffmann, a quienes personalmente
me gusta llamar ‘mi familia alemana’, a quienes quiero muchı́simo, ellos son
Michael, Martina, Ulrich y Bernhard.
Por último quiero agradecerles mucho mucho mucho a mi familia, quienes
me apoyaron durante toda mi vida e hicieron esto posible, a ellos los amo incondicionalmente y son Nélida Nazarre, Rodolfo Parrondo, Oscar Zacchigna, Ana
Marı́a D’Ambrosio, y Julia Zacchigna.
Índice general
1. Introducción
1.1. Ventajas de la paralelización . . . . . . . . . . .
1.2. Clasificación de los procesadores . . . . . . . .
1.3. Evolución de los procesadores . . . . . . . . . .
1.3.1. Inicio . . . . . . . . . . . . . . . . . . .
1.3.2. Paralelismo a nivel de instrucción . . . .
1.3.3. Limitaciones en el paralelismo a nivel de
1.3.4. Paralelismo a nivel de tarea . . . . . . .
1.4. Multi-Threading . . . . . . . . . . . . . . . . .
1.5. Multi-procesadores . . . . . . . . . . . . . . . .
1.6. Sistemas operativos y programación distribuida
1.6.1. Sistemas operativos . . . . . . . . . . .
1.6.2. Procesos e hilos . . . . . . . . . . . . . .
1.6.3. Planificación de hilos . . . . . . . . . . .
1.6.4. Programación distribuida . . . . . . . .
1.7. Consumo y frecuencia de trabajo . . . . . . . .
1.8. Conclusiones . . . . . . . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
instrucción
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
2
3
3
3
9
9
10
12
13
13
14
14
15
16
16
2. Teorı́a y diseño
19
2.1. Procesador Plasma y arquitectura MIPS . . . . . . . . . . . . . . 19
2.2. Comunicación entre los procesadores . . . . . . . . . . . . . . . . 20
2.3. Bus de interconexión . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.4. Arquitectura de la memoria . . . . . . . . . . . . . . . . . . . . . 24
2.4.1. Principio de localidad . . . . . . . . . . . . . . . . . . . . 24
2.4.2. Jerarquı́a de la memoria . . . . . . . . . . . . . . . . . . . 24
2.4.3. Memoria Cache . . . . . . . . . . . . . . . . . . . . . . . . 25
2.4.4. Arquitectura de la memoria en procesadores multi-núcleo 30
2.4.5. Protocolos y algoritmos de coherencia de cache . . . . . . 32
2.5. Manejo de interrupciones . . . . . . . . . . . . . . . . . . . . . . 33
2.6. Operaciones atómicas . . . . . . . . . . . . . . . . . . . . . . . . 34
2.7. Caracterización a priori del procesador . . . . . . . . . . . . . . . 39
3. Implementación y resultados obtenidos
3.1. Herramientas utilizadas . . . . . . . . .
3.2. Estructura del procesador multi-núcleo .
3.3. Controlador de memoria . . . . . . . . .
3.3.1. Descripción . . . . . . . . . . . .
3.3.2. Puertos de la entidad . . . . . .
vii
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
41
41
42
42
42
43
ÍNDICE GENERAL
viii
3.4. Plasma multi-núcleo . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . .
3.5. Árbitro del bus . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . .
3.6. Manejador de interrupciones . . . . . . . . . . . . . . . . . . . . .
3.6.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . .
3.6.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . .
3.7. Núcleo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.7.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . .
3.7.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . .
3.7.3. Descripción del algoritmo de coherencia de cache . . . . .
3.8. Unidad de control del núcleo . . . . . . . . . . . . . . . . . . . .
3.8.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . .
3.8.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . .
3.9. CPU plasma . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10. Caracterización del procesador . . . . . . . . . . . . . . . . . . .
3.10.1. Tamaño del procesador . . . . . . . . . . . . . . . . . . .
3.10.2. Desempeño en función del trabajo de las tareas . . . . . .
3.10.3. Desempeño en función de la utilización del bus . . . . . .
3.10.4. Tiempo de procesamiento en función del número de núcleos
3.10.5. Importancia de la memoria cache en procesadores multinúcleo . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
46
50
51
51
53
54
54
54
55
55
60
62
63
63
65
68
68
70
70
70
72
73
4. Conclusiones y trabajos futuros
75
4.1. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.2. Trabajos Futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Bibliografı́a
77
A. Implementaciones de spinLocks
79
A.1. Con soporte de hardware . . . . . . . . . . . . . . . . . . . . . . . 79
A.2. Solución propuesta por Peterson . . . . . . . . . . . . . . . . . . 80
A.3. Solución propuesta por el creador del PlasmaOS . . . . . . . . . 80
Capı́tulo 1
Introducción
En este capı́tulo se hace una breve introducción a la paralelización en el
contexto de los procesadores, para luego poder clasificar a los procesadores según
este criterio. Luego se realiza descripción de la evolución de los procesadores a lo
largo de la historia. Se muestran también las limitaciones que fueron apareciendo
y los cambios en el enfoque de los diseños de los mismos.
1.1.
Ventajas de la paralelización
El concepto de paralelismo es de gran importancia para este trabajo y lo
definiremos en nuestro caso como la forma de realizar dos acciones simultáneamente. Aprovechar el paralelismo es uno de los métodos más importantes para
mejorar el rendimiento en la ejecución de cualquier tarea, en particular en procesadores. Por ejemplo, al transferir información de un disco rı́gido, la velocidad
de transferencia está dada por el ancho de banda del disco, pero si se se colocan
dos discos y los cuales se acceden simultáneamente, se logra duplicar el ancho
de banda, y ası́ mejorar el rendimiento.
A nivel de ejecución de código hay distintos tipos de paralelismos y distintas
formas de explotarlo. Un primer ejemplo del uso del paralelismo es a nivel de
sistema, para lo que se pueden utilizar múltiples discos y/o múltiples procesadores. La carga de trabajo para atender las solicitudes de los clientes puede
ser repartida entre los procesadores y discos, y como consecuencia se obtiene
un mejor rendimiento. Cuando en un sistema es posible expandir la memoria
y el número de procesadores y discos se lo llama sistema escalable, y es una
propiedad muy valorada en los servidores.
Por otro lado a nivel de procesadores individuales, sacar provecho del paralelismo a nivel de instrucción es crı́tico para mejorar la capacidad de procesamiento, se explica con profundidad en la sección 1.3.2. Una de las formas
más simples de lograr esto es a través del uso de pipelining, que fue la técnica
utilizada en los primero micros para explotar el paralelismo. La idea básica del
pipeline, que se explica con mayor detalle en la sección 1.3.2, es solapar la ejecución de las instrucciones y ası́ reducir el tiempo total de ejecución para una
1
CAPÍTULO 1. INTRODUCCIÓN
2
secuencia de instrucciones. Un factor clave que da lugar al pipelining es que no
todas las instrucciones dependen de la anterior inmediata y por ello ejecutar la
instrucción parcialmente o completamente en paralelo es posible.
1.2.
Clasificación de los procesadores
A lo largo de la historia han surgido varias clasificaciones para los sistemas
computacionales. Muchas de ellas quedaron rápidamente obsoletas con el paso
del tiempo. Sin embargo, la clasificación propuesta por Flynn en el año 1966
[1] sigue siendo válida. La misma se basa en un modelo de dos dimensiones:
Stream de datos y Stream de instrucciones. Cada una de estas dimensiones
tiene dos posibles valores: Simple o Múltiple (en paralelo). Se obtienen ası́ cuatro
combinaciones posibles:
SISD: En inglés Single Instruction Single Data. Es el caso de un procesador
simple como el plasma.
SIMD: En inglés Single Instruction Multiple Data. Es el caso de las llamadas computadoras vectoriales o aquellas llamadas VLIW o EPIC.
MISD: En inglés Multiple Instruction Single Data. Son de uso poco frecuentes, algunos computadoras que entran en esta categorı́a son aquellas
que deben ser tolerantes a fallas, en las cuales existen múltiples procesadores trabajando sobre el mismo stream de datos, y que a la vez deben de
coincidir en el resultado.
MIMD: En inglés Multiple Instruction Multiple Data. Es el caso de los
multiprocesadores que se tratan en este trabajo.
Los SIMD y MISD son sistemas diseñados para usos especı́ficos, y no serán
tratados con mucha profundidad en este trabajo. Los SISD son los más simples
de los procesadores, y no sacan provecho ni de la paralelización de los datos
ni de las instrucciones, aunque si hacen uso de técnicas como pipelininig. Las
computadoras MIMD son las más flexibles dentro de esta clasificación, es posible
ejecutar dos streams de datos e instrucciones diferentes en simultáneo, sean
o no independientes entre sı́. Es claro también que deben existir al menos la
misma cantidad de tareas (o streams de instrucciones y datos) que de núcleos de
procesamiento para poder aprovechar las ventajas de una computadora MIMD.
Esto se explica en las siguientes secciones y de allı́ se concluye que el surgimiento
de procesadores MIMD es consecuencia del:
La necesidad de aumentar el rendimiento en la ejecución de tareas.
La limitación de los procesadores SISD, y de la paralelización posible en
las instrucciones.
La posibilidad de dividir las tareas en diferentes procesos independientes,
o poco correlacionados, lo que se llama paralelización a nivel de tareas.
A lo largo de este trabajo se tratan consecuencias y problemáticas de esta
paralelización.
CAPÍTULO 1. INTRODUCCIÓN
1.3.
1.3.1.
3
Evolución de los procesadores
Inicio
Los primeros procesadores, surgidos en la decada del 60, ejecutaban secuencialmente cada una de las instrucciones del código. La ejecución de cada instrucción esperaba la finalización de la instrucción predecesora antes de comenzar. No
hacı́an uso de ningún tipo de paralelización. El método utilizado para aumentar
su rendimiento consistı́a en aumentar la frecuencia del reloj, y los diseños de
las arquitecturas se apuntaban a reducir tiempo de propagación de las señales
en el procesador (es decir, cantidad de compuertas conectadas en cascada), y
de esa manera poder aumentar la frecuencia de trabajo. Otro método que se
utilizaba para llegar a ese mismo objetivo era mejorar la tecnologı́a del proceso
de fabricación de los circuitos integrados, reduciendo cada vez más el tamaño
de los transistores y ası́ poder aumentar su velocidad de trabajo [2]. Rápidamente se alcanzaron los lı́mites máximos para las tecnologı́as de la época, y se
comenzaron a estudiar otro tipo de maneras de seguir mejorando el rendimiento, basadas en la arquitectura del procesador en vez del proceso de fabricación.
La evolución en las arquitecturas se apuntaba a mejorar los rendimientos pero
siempre teniendo en cuenta los tipos de programas que se pretendı́a correr en
los procesadores.
Para medir el rendimiento de un procesador se utilizan programas llamados benchmark. Existen distintos benchmarks para evaluar distintos aspectos de
procesador. Por ejemplo, algunos evalúan operaciones en punto fijo, otras en
punto flotante, ancho de banda de la memoria, etc. Además existen distintos
benchmarks según el contexto para el que fueron diseñados, como pueden ser
computadoras de escritorio, servidores, sistemas embebidos, etc. Tras la ejecución de un benchmark, un cierto número de instrucciones han sido procesadas, y
una cierta cantidad de ciclos de reloj y un perı́odo de tiempo determinado han
transcurrido. A partir de estos tres valores surgen dos parámetros muy utilizados para evaluar a los procesadores, el CPI (en inglés Cicles Per Instruction)
que no tiene en cuenta el tiempo de ejecución y a la vez es independiente de la
frecuencia de reloj y el IPS (en inglés Instructions Per Second ) que no tiene en
cuenta la cantidad de ciclos que le lleva finalizar la ejecución de una instrucción.
La frecuencia de trabajo del procesador es a la vez el factor que vincula estos
dos valores.
En las siguiente secciones de este capı́tulo se muestra la evolución de las arquitecturas, junto con las mejoras y las problemáticas que implican. Finalmente
se muestran las nuevas tendencias en los procesadores, donde en algunos casos
especiales dejan de ser importantes los rendimientos individualmente y pasa a
ser fundamental la relación rendimiento/consumo.
1.3.2.
Paralelismo a nivel de instrucción
El paralelismo a nivel de instrucción o ILP (en inglés Instruction Level Paralelism) se refiere a la posibilidad de superponer la ejecución de más de una
instrucción en un programa. Estas instrucciones pueden ser consecutivas o no.
Un ejemplo de dos instrucciones que pueden ejecutarse en paralelo es:
1
2
add $ 1 , $ 2 , $3
sub $ 4 , $ 2 , $3
CAPÍTULO 1. INTRODUCCIÓN
4
Donde el resultado de sumar los regitros $2 y $3, se guarda en $1 y por otro
lado la resta de esos dos mismos registros se guarda en el registro $4. Las dos
operaciones pueden ejecutarse al mismo tiempo, sin ningún problema. Distinto
es en el siguiente ejemplo:
1
2
add $ 1 , $ 2 , $3
sub $ 4 , $ 1 , $5
Donde se ve que la segunda operación no puede ser ejecutada hasta que el resultado de primer instrucción no esté disponible. A estas instrucciones se las
llama dependientes, o también se dice que existe una dependencia entre las instrucciones. Existen varios tipos de dependencias entre instrucciones, pueden ser
de datos, de control o de hardware Estas dependencias se detallan en profundidad en este trabajo. Para programas tı́picos en assembler MIPS, en bloques
de programa donde no hay saltos ni bucles(salvo por el salto para llegar a ese
bloque y el salto para volver) el porcentaje tı́pico de instrucciones paralelizables
se encuentra entre un 15 % y un 20 % [2]. Al ejecutar este tipo de bloques en
un procesador con capacidad de paralelismo no brinda una mejora significativa.
Para bloques de programa donde existen ramificaciones y bucles, este porcentaje
de instrucciones paralelizables es mucho mayor y hacer provecho de la paralelización a nivel de instrucción se vuelve una de las maneras más eficientes para
mejorar el rendimiento.
La ejecución de instrucciones en forma superpuesta implica modificaciones
en la arquitectura del procesador:
Pipelining: Cuando la lógica utilizada para la ejecución de una instrucción
no puede seguir siendo reducida, el pipelining es una técnica que surge naturalmente. Se basa en dividir la ejecución de la instrucción en etapas,
donde cada una de estas etapas tiene una cantidad mucho menor de compuertas conectadas en cadena. De este modo se logra bajar el tiempo de
propagación en cada etapa, lo cual hace posible el aumento de la frecuencia
del reloj. Estas etapas son interconectadas por registros (flip-flops), que
tienen la función de guardar los resultados de cada etapa y transferirlo a la
siguiente en cada ciclo de reloj [3]. Las cinco etapas tı́picas y los registros
utilizados para interconectarlas se muestran en la figura 1.1. Las etapas
pueden superponerse. En la figura 1.2(a) se ve un ejemplo de un pipeline
de una arquitectura RISC, donde la ejecución de la instrucción se divide
clásicamente en cinco etapas:
• IF Instruction Fetch
• ID Instruction Decode
• EX Execution
• MEM Memory Access
• WB Write Back
En este tipo de arquitecturas existe lo que se llaman stalls: son momentos donde alguna etapa del pipeline debe ser pausada por existir alguna
dependencia entre dos instrucciones consecutivas. Este tipo de arquitecturas no muestra mejoras en el CPI, de hecho en general lo empeoran,
pero al lograr un aumento en la frecuencia de reloj, se puede mejorar el
IPS. El CPI empeora debido a los Stalls que se generan en el procesador.
EX
MEM
REGISTROS
5
REGISTROS
ID
REGISTROS
IF
REGISTROS
IF
CAPÍTULO 1. INTRODUCCIÓN
WB
CLK
Figura 1.1: Pipeline de cinco etapas interconectadas con registros. En
esta figura se muestran las cinco etapas en las que se divide clásicamente una
instrucción de una arquitectura RISC, y como se interconectan mediante registros para poder reducir los tiempo de propagación en las etapas individualmente
y ası́ poder aumentar la frecuencia de trabajo.
En arquitecturas más evolucionada se utilizan técnicas como forwarding
y bypassing, que permiten evitar algunos de los stall. Cómo se producen
los stalls y las maneras que existe de evitarlos son temas ampliamente
explicados en [3]
Multiple Instruction Issue: El procesador ejecuta realmente varias instrucciones simultáneamente. Para ellos algunas partes del Hardware deben ser
multiplicada, como por ejemplo las ALU. En la figura 1.2(b), se ve como
se realiza la ejecución de las instrucciones. Se muestra la diferencia con el
ejemplo de la figura 1.2(a), donde en cada ciclo de reloj se ejecutan dos
veces cada una de las etapas del pipeline. El uso de este recurso mejora el
CPI. En el ejemplo de la figura 1.2(b) el CPI puede llegar a ser reducido
a la mitad, al ser posible completar hasta dos instrucciones por ciclo. De
esta manera se vuelve posible también alcanzar valores de CPI menores
a la unidad. En los pipelines más mas evolucionados los stalls debidos
a dependencias entre instrucciones son prácticamente eliminados, lo que
mejora notablemente el ILP.
Como se dijo este tipo de soluciones permiten la ejecución superpuesta de
instrucciones, pero a veces la superposición no es posible debido a las dependencias existentes entre las instrucciones. A veces estas dependencias dependen
sólo del orden de las instrucciones, por ejemplo en el siguiente código:
1
2
3
add $ 1 , $ 2 , $3
sub $ 4 , $ 1 , $5
sub $ 6 , $ 2 , $3
las instrucciones 1 y 2 son dependientes. Un procesador con dos issue slots 1
tiene dos ramas de ejecución de instrucciones. La rama que ejecute la segunda
instrucción, deberá ser pausada en algún momento debido a la dependencia
existente. Para evitar la pausa que se produce en la ejecución se puede modificar
el orden de las instrucciones por el siguiente:
1
2
add $ 1 , $ 2 , $3
sub $ 6 , $ 2 , $3
1 No tomamos como ejemplo un procesador que sólo posee un pipeline, porque como se dijo
antes las dependencias entre instrucciones pueden ser evitadas en esas arquitecturas.
CAPÍTULO 1. INTRODUCCIÓN
6
INSTRUCCIONES
Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo
1
3
4
5
6
7
8
9
2
INSTRUCCIÓN 1
INSTRUCCIÓN 2
INSTRUCCIÓN 3
INSTRUCCIÓN 4
INSTRUCCIÓN 5
IF
ID
IF
EX MEM WB
ID
EX MEM WB
IF
ID
EX MEM WB
IF
ID
EX MEM WB
IF
ID
EX MEM WB
TIEMPO
(a) Arquitectura con pipeline de cinco etapas.
INSTRUCCIONES
Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo
1
2
3
4
5
6
7
8
9
INSTRUCCIÓN 1
INSTRUCCIÓN 2
INSTRUCCIÓN 3
INSTRUCCIÓN 4
INSTRUCCIÓN 5
INSTRUCCIÓN 6
INSTRUCCIÓN 7
INSTRUCCIÓN 8
INSTRUCCIÓN 9
INSTRUCCIÓN 10
IF
IF
ID
ID
IF
IF
EX MEM WB
EX MEM WB
ID
EX MEM WB
ID
EX MEM WB
IF
ID
EX MEM WB
IF
ID
EX MEM WB
IF
ID
EX MEM
IF
ID
EX MEM
IF
ID
EX
IF
ID
EX
WB
WB
MEM WB
MEM WB
TIEMPO
(b) Arquitectura con pipeline de cinco etapas y dos issue.
Figura 1.2: En la figura 1.2(a) se puede apreciar como las etapas de las diferentes
instrucciones se suporponen. En la figura 1.2(b) se muestran como se superponen
las etapas e instrucciones de un procesador con el mismo pipeline de cinco etapas
y dos issue slots.
3
sub $ 4 , $ 1 , $5
esta porción de código tiene mayor ILP que la anterior.
Para aumentar el ILP de un programa se puede entonces reordenar las instrucciones. Para poder hacer un reordenamiento y ası́ hacer uso eficiente del
ILP, existen dos enfoques claramente diferenciables:
Basado en Software: Este enfoque encuentra posibles instrucciones paralelizables en forma estática, en tiempo de compilación de un programa. Es
la forma más básica de hacerlo. Esto depende del compilador, que básicamente reacomoda instrucciones de forma tal de bajar el porcentaje de
instrucciones con dependencias. Como se dijo antes en la ISA de MIPS no
se especifica la implementación, por lo que el compilador no tiene porque
saber la arquitectura del mismo, pero si puede explicitarse, de manera
que tenga en cuenta la arquitectura del microprocesador, como puede ser
si cuenta o no con unidades de multiplicación de punto fijo, unidades de
multiplicación de punto flotante, etc.
Basado en Hardware: Este enfoque encuentra posibles instrucciones paralelizables en forma dinámica, en tiempo de ejecución de un programa.
Es decir, las instrucciones no necesariamente ingresan en el orden en el
CAPÍTULO 1. INTRODUCCIÓN
7
que se encuentran en el código, sino que el procesador puede ‘ver’, la instrucción a la que apunta el contador de programa y un número dado de
instrucciones posteriores a esa. A estas instrucciones se las llama ventana de programa. De entre todas las instrucciones que se encuentran en
esa ventana del programa, el procesador elige las que son mejores para su
ejecución en paralelo.
En un caso general estos dos enfoques pueden ser explotados en forma simultánea, logrando ası́ mejores resultados en los valores de CPI e IPS.
A modo de ejemplo se presentan brevemente dos técnicas simples utilizadas
para la planificación estática:
Loop unrolling: Incrementa el número de operaciones que se ejecutan dentro de un bucle y a la vez reduce el número de iteraciones. En los bucles
es donde en general se encuentra un mayor número de instrucciones independientes. El ejemplo clásico es el de la suma de dos vectores, donde
en vez de sumar una componente, se suman dos o más de las componentes por iteración y ası́ se reduce la cantidad de veces que se ejecutan las
instrucciones del bucle. Una consecuencia de realizar esto es que el código
aumenta su tamaño ocupando mayor espacio en la memoria.
Pipeline Scheduling: Dado que hay distintos tipos de dependencias, existen
pares de instrucciones que tienen dependencias reales y otros que pueden
ser evitados. Las instrucciones pueden ser planificadas teniendo en cuenta
este factor y de esa manera evitar pausas en la ejecución. Esta técnica
depende de la arquitectura.
Existen otras técnicas de hardware que se utilizan para mejorar el rendimiento, reduciendo los stalls, mejorando la planificación dinámica, aumentando
el número de instrucciones paralelizables, prediciendo posibles saltos y especulando que instrucciones se ejecutarán. Se explican a continuación algunas
brevemente:
A continuación describimos brevemente algunas de las técnicas más conocidas para mejorar el rendimiento. Los objetivos son evitar stalls y pausas en
en los procesadores, mejorar la planificación dinámica, aumentar el número de
instrucciones paralelizables, y predecir el flujo del programa correctamente:
Forwarding and bypassing: Se utiliza en los pipelines para evitar stalls al
ejecutar instrucciones que poseen dependencias de datos. Se basa en extraer los datos antes de que terminen de atravesar el pipeline. Por ejemplo,
se extrae el resultado de una operación aritmética apenas termina la etapa
de ejecución en vez de esperar a que se escriba en un registro.
Branch prediction (local and global): Cuando en un programa hay un salto condicional (o bifurcación) las instrucciones que se deben de ejecutar
cambian según se cumpla o no la condición de salto. Esta técnica se basa
en ir cargando en el procesador las instrucciones más probables y ellas
dependen de la probabilidad de saltar o no. En un bucle lo más usual es
que al llegar al final se salta de nuevo al principio. Este salto se realiza
N veces, de las cuales N − 1 veces se salta y sólo una vez se sigue de
largo. Para todo bucle con N mayor a uno las probabilidades de salto son
mayores a las de no saltar.
CAPÍTULO 1. INTRODUCCIÓN
8
Register renaming: Se generan registros extra aparte de los definidos en
la ISA MIPS, estos registros no pueden ser accedidos por los usuarios,
en cambio sı́ internamente por el procesador y en algunos casos se logra
mejorar el rendimiento. Un ejemplo simple serı́a el siguiente:
1
2
3
add $ 1 , $ 2 , $3
sw $ 1 , 0 x10 ( $ sp )
li
$ 1 , 0 x1
Las instrucciones dos y tres tienen una dependencia, de modo que la instrucción tres no puede ser ejecutada antes que la dos, ya que se estarı́a
guardando un valor erróneo, pero el registro $1 es cargado con un valor
distinto en la tercer instrucción. Si fuese posible cambiar $1 por $4 en las
dos primeras instrucciones, entonces sı́ serı́a posible ejecutar las instrucciones dos y tres en paralelo. Esto ocurre a veces por falta de registros
en la arquitectura, que es una de las condiciones del procesador perfecto:
infinito número de registros en el procesador. A veces no se puede simplemente diseñar un procesador con mayor número de registros, ya que esta
caracterı́stica puede estar definida en la arquitectura de instrucciones (como es el caso de MIPS), pero los procesadores sı́ pueden tener un número
mayor de registros en su arquitectura e internamente utilizarlos como se
explico antes, renombrándolos para lograr mayor ILP.
Teniendo en cuenta lo dicho anteriormente se puede hacer una clasificación
de los núcleos de procesamiento a partir de la arquitectura del procesador, sin
especificar si posee un núcleo o varios de ellos. En ésta clasificación también se
identifica a categorı́a pertenece de la clasificación mostrada anteriormente en la
sección 1.2:
Procesador escalar: Los procesadores más simples, pueden estar implementados con un pipeline o no. Clasificado como SISD.
Procesador super-escalar con planificación estática: Procesadores que utilizan Multiple Instruction Issue, en general utilizan una arquitectura con
pipeline. La planificación de las instrucciones es estática, o sea que solo se
realiza a nivel de compilación. Clasificado como MIMD.
Procesadores vectoriales que son clasificados como SIMD, utilizados principalmente en aplicaciones cientı́ficas, que generalmente realizan muchas
veces la misma operación entre distintos datos, por ejemplo al hacer operaciones entre vectores de gran tamaño.
Procesador de arquitectura VLIW (en inglés Very Long Instruction Word )
o EPIC (en inglés Explicit-Parallel Instruction Computer ): Este tipo de
procesadores son diferentes a los que se han presentado en este trabajo. Las
instrucciones que ejecutan los mismos indican en forma explı́cita la operación que debe realizar cada una de las unidades funcionales del procesador.
Tienen dos ventajas frente a los procesadores de planificación dinámica:
Una alta reducción en el hardware necesario para su implementación y
que los compiladores pueden generar código de manera que se aproveche
al máximo las ventajas del paralelismo. Este tipo de procesadores son del
tipo de MIMD.
CAPÍTULO 1. INTRODUCCIÓN
9
Procesador super-escalar con planificación dinámica: Procesadores que utilizan Multiple Instruction Issue, en general utilizan una arquitectura con
pipeline. Aprovecha tanto las planificación estática lograda a nivel compilación y la planificación dinámica realizada a nivel de ejecución. La operación
que ejecuta cada unidad funcional del procesador se define luego de decidir
que instrucciones se ejecutan. Este tipo de procesadores se clasifican como
MIMD.
La mayorı́a de los núcleos de los procesadores que se utilizan hoy en dı́a son
superescalares aunque no todos tienen planificación dinámica de instrucciones,
debido a la gran complejidad que esto presenta.
1.3.3.
Limitaciones en el paralelismo a nivel de instrucción
En un procesador ideal que aprovecha completamente el ILP presente en un
programa, los lı́mites en el rendimiento son impuestos por los flujos de datos a
través de los registros o memoria.
Un procesador ideal contarı́a con infinitos registros, una ventana de programa
infinita, perfectas predicciones en el flujo de ejecución del programa, un perfecto
análisis de aliasing de memoria2 y accesos a memoria que solo necesiten un ciclo
de reloj. En un procesador real estas condiciones no se cumplen, y por eso se
alcanza el lı́mite en el rendimiento de un mono-procesador, cuando el costo de
hardware es demasiado, y la mejora que su complejización proporciona no es
significativa.
1.3.4.
Paralelismo a nivel de tarea
En ocasiones se habla de paralelismo a nivel de tarea o hilo (TLP, en inglés
Thread Level Paralelism). La definición precisa de hilos, y la de procesos también, se puede encontrar en [10]. Para explicar TLP basta con tener una pequeña
noción de lo que es un hilo o un proceso, para este caso utilizaremos el nombre
tarea independientemente de que sea un hilo o un proceso. Una tarea es una
porción de programa que cumple con un objetivo especı́fico. Hay tareas que son
independientes entre sı́ y tareas que poseen dependencias con otras. El ejemplo
clásico para dos tareas pseudo-independientes lo encontramos en un servidor al
cual acceden N usuarios y hacen N pedidos distintos. En general estas tareas
pueden ser ejecutadas todas por separado y en cualquier orden. Un corolario
de poder ejecutar las tareas por separado y en cualquier orden es que también
pueden ser ejecutadas en paralelo.
Los procesadores MIMD pueden explotar las ventajas del TLP y hay dos
enfoques para explotarla:
Los multi-procesadores: El enfoque se basa en multiplicar el número de
núcleos e interconectarlos de manera eficiente, sin que importe la arquitectura del núcleo en sı́ (ver sección 1.5). Pueden clasificarse en dos grandes
2 Es una técnica para reducir accesos a memoria durante el proceso de optimización en
tiempo de compilación. Detecta uno de los dos casos posibles, si existe o no aliasing entre
punteros, es decir punteros que apuntan a una misma posición de memoria. Una vez detectado
es posible realizar las optimizaciones necesarias, si no se puede asegurar ninguno de los dos
casos, las optimizaciones no pueden ser realizadas.
CAPÍTULO 1. INTRODUCCIÓN
10
grupos, los procesadores simétricos y los asimétricos. La principal diferencia entre los dos grupos es la forma en la que se interconectan los distintos
procesadores, como se comunican y como se distribuye la memoria, como
se ve en la sección 2.2.
Los procesadores que soportan multi-threading(MT) y simultaneous-multithreading(SMT): Es un enfoque en el que un único núcleo soporta la ejecución de más de una tarea en simultáneo, o que tiene la capacidad de
intercambiar tareas de forma muy rápida, ya que almacena contextos de
más de una tarea [2]. En la sección 1.4 se profundiza el tema.
1.4.
Multi-Threading
El concepto de MT o Multi-Threading y SMT del inglés Simultaneous MultiThreading tiene lugar sólo si existen varias tareas en ejecución, que poseen poca
dependencia entre sı́. Durante la ejecución de una tarea se puede llegar a tener
un stall que dure una cantidad significativa de ciclos de reloj, pero no tan
grande como para que convenga realizar un cambio de contexto3 . Es el caso de
una instrucción de multiplicación/división entera, o en punto flotante, que suele
tomar varios ciclos de reloj antes de entregar el resultado. La situación anterior
da lugar a lo que se llama multi-threading, lo que implica almacenar el contexto
de múltiples tareas en el núcleo, duplicando hardware necesario, tal como los
registros y el contador de programa. De esta manera un cambio de contexto
entre estas dos tareas tiene costo nulo y se vuelve provechoso cambiar de tarea
cuando existen estos stalls de corto tiempo.
Visto de otra manera, lo que se tiene son varias ventanas de programas en
tareas distintas y se toman instrucciones o bien de una o de la otra. Hay dos
tipos de multi-threading, el grueso y el fino. En el grueso, se realiza un cambio
de contexto cada vez que hay un stall que lo amerite. En el fino, se cambia de
tarea en cada ciclo.
La desventaja del grueso, es que espera a que exista un stall antes de cambiar
de tarea, con lo que se pierde un ciclo. Durante este ciclo de reloj se identifica
el stall y un ciclo posterior se procede al cambio de contexto. En el fino se
cambia de tarea en cada ciclo, lo cual no impide que puedan ocurrir stalls, pero
sı́ ocurren con menor frecuencia. Una desventaja del fino es que la latencia de
ejecución de una instrucción en una tarea aumenta: En el ejemplo de la figura
1.3, al ejecutar las tareas A y B en un MT grueso, las seis primeras instrucciones
comienzan su ejecución entre los ciclos uno y tres, mientras que si se utiliza un
MT fino, las seis primeras instrucciones recién comienzan a ejecutarse entre el
primero y el noveno ciclo.
En multi-threading se pueden tomar instrucciones únicamente de una sola
ventana de programa (o sea de una sola tarea) en cada ciclo de reloj. En cambio
en Simultaneous multi-threading, en cada ciclo de reloj pueden ser tomadas instrucciones de distintas ventanas, esto se muestra en la figura 1.3 en comparación
con el MT fino y grueso. En general existe una dependencia notablemente menor entre instrucciones de distintas tareas, de esta manera se puede sacar mayor
3 Un cambio de contexto es el proceso que realiza el sistema operativo al cambiar de una
tarea a otra, para ello debe guardar el estado de la tarea en ejecución y cargar el estado de la
tarea que será ejecutada. Se explica con mayor detalle en la sección 1.6.1.
CAPÍTULO 1. INTRODUCCIÓN
11
TAREA B
TAREA C
TAREA D
ISSUE
SLOTS
ISSUE
SLOTS
ISSUE
SLOTS
ISSUE
SLOTS
TIEMPO
TAREA A
(a) Threads.
MT FINO
SMT
ISSUE
SLOTS
ISSUE
SLOTS
ISSUE
SLOTS
TIEMPO
MT GRUESO
(b) Multi-Threading.
Figura 1.3: Como cuatro tareas usan los issue slots de un procesador superescalar en diferentes enfoques. La figura 1.3(a) muestra como cada tarea utiliza
los issue slots en un procesador superescalara estándar sin soporte para MT.
Los ejemplos de la figura 1.3(b) muestran tres opciones de MT. Se observa que
las tareas se ejecutan en conjunto. El eje horizontal representa los issue slots
disponibles en cada ciclo de reloj, en este caso cuatro. El vertical el tiempo en
ciclos de reloj. Coarse-MT cambia la tarea en ejecución al producirse un stall
(pierde un ciclo en cada cambio). Fine-MT cambia la tarea en ejecución en cada
ciclo (aumenta la latencia de ejecución entre dos instrucciones de una misma
tarea). SMT utiliza instrucciones de las cuatro tareas en cada ciclo de reloj,
haciendo mejor uso del núcleo (aumenta la latencia como en fine-MT pero en
menor meida).
CAPÍTULO 1. INTRODUCCIÓN
12
provecho de la habilidad del procesador de ejecutar instrucciones en paralelo.
La dependencia entre las instrucciones es el factor que limita la paralelización
de instrucciones y con esta técnica se logran grandes mejoras. Entonces se puede decir que el multi-threading es una técnica que aprovecha ILP y TLP en
conjunto.
Teniendo en cuenta este técnica a los procesadores super-escalares se los
puede subclasificar en:
Superscalar con multithreading grueso.
Superscalar con multithreading fino.
Superscalar con multithreading simultáneo.
Este último tipo de procesadores son usualmente los más poderosos y más
grandes en tamaño. No se utilizarán para el análisis de multi-procesadores en
este trabajo por una limitación en la capacidad de las FPGAs.
1.5.
Multi-procesadores
Sin importar si el núcleo saca provecho de ILP, TLP, MT fino o grueso o
SMT, los procesadores múlti-nucleo tienen la ventaja de aprovechar el paralelismo a nivel de tarea que se encuentra en las aplicaciones. Es decir que se
aprovecha la TLP independientemente de la arquitectura del núcleo de procesamiento, ya que se tiene más de un núcleo de procesamiento, cada uno con sus
registros, contador de programa y corriendo una tarea especı́fica, cada uno con
un contexto distinto.
Los distintos núcleos, o mejor dicho las tareas que corren en ellos, deben
comunicarse entre sı́ y además compartir otros recursos de hardware. Existen
distintas arquitecturas para lograr esto, que se tratan en el desarrollo de este
trabajo. En la figura 1.4 se contrasta un procesador mono-núcleo con uno de
múltiples núcleos, que son conectados a través de un bus simple.
El hecho de compartir la memoria trae varias complicaciones. Por ejemplo,
resulta necesario una entidad que la administre de forma correcta. Cada núcleo
se comunicará con esta entidad antes de poder realizar accesos a memoria.
En casos donde haya más de un procesador queriendo hacer uso de los recursos uno de ellos deberá ser pausado hasta que alguno de ellos finalice y la
entidad que los administra brinde acceso al siguiente núcleo. Este problema se
intensifica a medida que se aumenta el número de núcleos y que los programas
tienen mayor cantidad de instrucciones que solicitan acceso a los recursos de
hardware.
Dado que uno de los recursos más solicitado es el acceso a memoria, la implementación de una memoria cache brinda una técnica para disminuir el tráfico
que se genera. El uso de memorias cache trae otros beneficios y complicaciones
que se detallaran más adelante en la sección 2.4.3.
Por último, se deberá diseñar un controlador de interrupciones distinto al
utilizado en procesadores con un único núcleo, porque los procesadores pueden en principio ser interrumpidos por separado. La tarea de este controlador
será identificar a que procesador corresponde enviar cada solicitud de interrupción y esto se presentará en la sección 2.5.
CAPÍTULO 1. INTRODUCCIÓN
13
Figura 1.4: Procesadores de con un solo núcleo vs procesadores con varios
núcleos
1.6.
1.6.1.
Sistemas operativos y programación distribuida
Sistemas operativos
Un sistema informático consiste en uno o mas procesadores, una memoria
principal, y dispositivos de entrada/salida. Todos estos dispositivos forman un
sistema complejo. Escribir un programa que realice un buen seguimiento de
todos estos componentes, y que a su vez los utilice en forma correcta y de
una manera eficiente, es un trabajo de extrema dificultad. Por estas razones la
tendencia actual es equipar a estos sistemas con una capa de software llamada
sistema operativo, cuyo trabajo es administrar todos los dispositivos y brindar a
los programas escritos por los usuarios una interfaz simplificada con el hardware.
Los sistemas operativos se diseñan prestando especial atención al sistema
en el que van a correr, servidores, computadoras de escritorio, sistemas embebidos, etc. Se tienen en cuenta muchos factores para su diseño: flujo de datos
de entrada/salida, carga de procesamiento, simetrı́a o asimetrı́a de los distintos
núcleos de procesamiento, etc. Un factor que interesa nombrar para este trabajo
es que existen sistemas operativos de tiempo real o RTOS(en inglés Real-Time
Operating Sistems). Entre ellos se pueden diferenciar dos tipos: Hard RTOS, en
los cuales las respuestas deben ocurrir en un momento exacto y los Soft RTOS,
donde es aceptable no cumplimiento ocasional de algún plazo. Un ejemplo de los
Soft RTOS son los sistemas operativos multimedia, que reproducen audio y/o
video, mientras que uno para los hard RTOS es un sistema operativo que corre
en un sistema computacional que controla una lı́nea de producción, en donde se
interactúa con espacios fı́sicos y los movimientos deben de estar perfectamente
coordinados.
CAPÍTULO 1. INTRODUCCIÓN
1.6.2.
14
Procesos e hilos
Se llama proceso a una tarea que debe ejecutar el sistema operativo. Es una
porción de código con un fin en particular. Puede funcionar en conjunto con
otros procesos o no.
Un proceso puede ser dividido a su vez en hilos, que siguen siendo una porción
de código con un fin en particular, pero este conjunto de hilos que forman el
proceso tiene la particularidad de compartir el mismo espacio de memoria, a
veces llamado también contexto. Estos hilos trabajan entonces en un mismo
rango de memoria de programa y de datos.
Al intercambiar un proceso que se ejecuta en un núcleo, el contexto también debe cambiar, mientras que al intercambiar entre hilos, no. Un cambio de
contexto de memoria suele tener un gran costo computacional, y esta es la principal diferencia entre los hilos y procesos. Un cambio en el hilo que se ejecuta
tiene un costo computacional mucho menor al cambio de proceso. Un ejemplo
del costo de un cambio de contexto está relacionada con las memorias cache
(ver sección 2.4). Como dos procesos suelen trabajar en espacios de memoria
distintos, los datos e instrucciones deben ser cacheados nuevamente, generando
muchos misses los cuales suelen tener un costo grande de tiempo. El contexto
incluye también a los registros del procesador, ellos sı́ deben de ser almacenados
antes de cambiar de hilo o proceso.
1.6.3.
Planificación de hilos
Una de las capacidades de los OS es poder planificar las tareas en ejecución.
Hay dos protocolos populares:
Planificación Round-Robin: Reparte el tiempo de ejecución equitativamente entre todas las tareas. Primero se corre la primer tarea un perı́odo de
tiempo preestablecido, luego la segunda tarea la misma cantidad de tiempo, ası́ hasta llegar a la última y se vuelve a empezar con la primera.
Planificación preventiva: A cada tarea se le asigna una prioridad y cada
una de ellas tiene un estado: ‘lista’, ‘bloqueada’ o ‘en ejecución’. En la
figura 1.5, se ven los estados y transiciones posibles de las tareas. Cuando
se decide cambiar la tarea en ejecución se selecciona a la tarea en estado
‘lista’ que tenga mayor prioridad, esta será la tarea que pasará a estar
Figura 1.5: Estados y transiciones posibles para los hilos de un sistema operativo.
CAPÍTULO 1. INTRODUCCIÓN
15
en el estado de ejecución4 . Para sacar una tarea en ejecución existen dos
motivos: que el OS decida que ya estuvo suficiente tiempo en ejecución o
que la tarea se bloquee. Una tarea sólo puede pase al estado ‘bloqueada’
si está en estado ‘en ejecución’. Las razones para bloquear una tarea son:
• La tarea se bloquea por ‘voluntad’ propia, cuando decide que ya
realizó suficiente procesamiento libera el CPU para que otra tarea
pueda ejecutarse.
• La tarea se bloquea al querer utilizar un recurso de hardware ocupado.
El OS es entonces una pieza fundamental en todo sistema computacional
que trabaje con múltiples tareas ya que es quien planifica las distintas tareas
según prioridad y en función de los recursos disponibles, de esta manera el
usuario/programador ocuparse solamente de programar las tareas en sı́ y no en
como se debe alternar su ejecución.
1.6.4.
Programación distribuida
La programación distribuida se basa en dividir las tareas que se deben ejecutar en la mayor cantidad de subtareas independientes. Este paradigma/enfoque
de programación es impulsado por la aparición de los multi-procesadores, que
como vimos tiene la capacidad de correr tareas en paralelo y cuanto mayor sea la
independencia entre tareas mayor será el aumento en el rendimiento. Este tipo
de enfoque es el que se le está dando hoy en dı́a a la programación. El principal
problema de este estilo de programación es que no ha habido grandes avances
en este área y los compiladores no brindan el soporte suficiente para hacer este
tipo de programación en forma eficiente. El trabajo de subdividir las tareas y
lograr la menor dependencia posible queda pura y exclusivamente a cargo de los
programadores.
Si se lograra hacer aplicaciones que sean cada vez más distribuidas, la eficiencia de los procesadores aumentarı́a significativamente, simplemente incrementando el número de núcleos en un procesador. Como se explicó anteriormente,
en aplicaciones de servidores sı́ es posible distribuir eficientemente la carga de
trabajo en múltiples tareas. Los sistemas computacionales orientados a servidores suelen tener un número grande de núcleos comparados con otro tipo de
sistemas. En ese caso se habla de sistemas de multi-procesadores de gran escala.
La particularidad de estas aplicaciones es que pueden ser divididas en tareas
muy poco acopladas, es decir intercambian poca información entre sı́.
Existen otro tipo de aplicaciones más acopladas, donde el intercambio de
datos es mayor. En este tipo de tareas es donde los programadores tienen un
desafı́o mayor, que es el de dividir procesos en hilos independientes. Estas aplicaciones son comunes en sistemas de escritorio y también en sistemas embebidos.
Los procesadores multi-núcleo son los más populares en este área, y como diremos en breve las tendencias apunta a seguir aumentando en número los núcleos
de un procesador. Los programadores se ven obligados a implementar la programación distribuida en caso de querer implementar aplicaciones con un buen
4 En un procesador con un núcleo de procesamiento y sin multi-threading hay una única
tarea en estado ‘en ejecución’, para uno con dos núcleos hay dos, para los procesadores con
MT depende de la cantidad de contextos que pueda almacenar cada núcleo.
CAPÍTULO 1. INTRODUCCIÓN
16
desempeño, y éste es uno de los principales problemas de la informática hoy en
dı́a.
1.7.
Consumo y frecuencia de trabajo
El consumo de un circuito digital esta dado por [5]:
P otdisipada ∝ f × CL × V 2
Donde CL es una capacidad asociada a los transistores, V es la tensión de
alimentación y f es la frecuencia de trabajo. La limitación para aumentar la
frecuencia de trabajo se basa en la potencia que se puede disipar. Años atrás
alcanzaba con reducir el tamaño de los transistores, y de esa manera se reducı́a
la capacidad y también se bajaba la tensión de alimentación y eso permitı́a
aumentar la frecuencia. Hoy en dı́a existen grandes dificultades para seguir reduciendo el tamaño de los transistores, lo que hace que sea difı́cil aumentar la
frecuencia de trabajo sin que aumente el consumo. Entonces resulta más conveniente aumentar el número de núcleos de procesamiento y hacerlos funcionar
en conjunto.
Otra lı́nea de trabajo que existe hoy en dı́a busca bajar el consumo de los procesadores al mı́nimo. Por ello tampoco siempre se utiliza la máxima frecuencia,
dejando el rendimiento de lado y el consumo pasa a ser la principal preocupación. Los procesadores más nuevos son diseñados con la capacidad de cambiar
la frecuencia de trabajo según el uso que se le este dando al mismo. Cuando se
requiere mayor rendimiento se aumenta la frecuencia de trabajo, mientras que
cuando se quiere un ahorro de energı́a se disminuye.
Otro método que se utiliza para que el rendimiento y consumo de los procesadores sea parametrizable (controlable), es a través del aprovechamiento de
los múltiples núcleos que se tiene en un procesador multi-núcleo. Cuando se
quiere un consumo bajo, se apagan los núcleos que no son necesarios, y cuando
se quieren un rendimiento alto se los enciende. En ocasiones se prefiere tener
un número mayor de núcleos de bajo consumo en vez una menor cantidad de
núcleos de alto rendimiento.
Otro caso especial que se da es implementar procesadores con núcleos asimétricos, con distinto poder de procesamiento y consumo. Dependiendo de la necesidad se utilizan núcleos de alto, mediano o bajo consumo/rendimiento en
conjunto dentro del mismo procesador.
La utilización de todas estas técnicas en conjunto (cambio en la frecuencia de trabajo, asimetrı́a y apagado y encendido de los núcleos) dan lugar a
procesadores de alto rendimiento y muy bajo consumo al mismo tiempo.
1.8.
Conclusiones
Históricamente los enfoques apuntaban a mejorar la arquitectura del núcleo
de procesamiento: Se aumentó la frecuencia de trabajo, se mejoró la tecnologı́a
de fabricación de transistores. Luego se empezó a explotar ILP. Se desarrollo
el pipeline, luego procesadores con multiple issue slots. Posteriormente multithreading y SMT. Las tendencias de hoy en dı́a apuntan a mejorar los multiprocesadores y las técnicas de programación distribuida, lo que a la vez permite
CAPÍTULO 1. INTRODUCCIÓN
17
aumentar el número de núcleos de un procesador, y ésto último impulsa el
desarrollo en mejores técnicas de programación distribuida. Se forma un ciclo
que se realimenta, e impulsa a los procesadores multi-núcleo. Por otro lado el
diseño del núcleo en sı́ ha alcanzado niveles, en los cuales se vuelve difı́cil seguir
avanzando, multiplicar el número de núcleos en vez de rediseñarlos desde el
comienzo resulta conveniente. El consumo de los procesadores se vuelve un factor
muy importante y fomenta también el diseño enfocado a los multi-procesadores.
Y como se dijo antes utilizando este enfoque se puede lograr un bajo consumo
y un gran rendimiento, por eso esta tesis se centra en los multi-procesadores.
CAPÍTULO 1. INTRODUCCIÓN
18
Capı́tulo 2
Teorı́a y diseño
En este capı́tulo se realiza una introducción al procesador plasma junto con
la arquitectura de instrucciones que soporta. Se evalúan las distintas posibilidades para el diseño del multi-procesador. El tema principal del diseño es la
arquitectura de la memoria y la comunicación entre los procesadores. Otros temas secundarios que se tratan son las interrupciones en multi-procesadores y la
dificultad de implementar operaciones atómicas en los mismos. Se fundamentan
las decisiones tomadas, las cuales a lo largo del trabajo apuntan a lograr un
diseño lo más simple posible.
2.1.
Procesador Plasma y arquitectura MIPS
El micro-procesador plasma es un procesador diseñado en VHDL1 , sintetizable, RISC de 32 bits, ejecuta todas las instrucciones de modo usuario de la
ISA MIPS(TM) salvo las de acceso desalineado a memoria2 .
En la página http://opencores.org/project,plasma se puede descargar la última versión de este procesador. Esta implementación incorpora también algunos
periféricos. Fue probado en distintas FPGAs de Xilinx y Altera, pero sobretodo
en FPGAs Spartan-3E de Xilinx, en el kit de Digilent ”Spartan-3E Starter Kit”.
La arquitectura del procesador plasma implementa la unidad de procesamiento
en dos o tres etapas de pipeline a elección, una memoria cache opcional de 4kB.
La frecuencia de trabajo al implementarlo en FPGAs de Xilinx y Altera ha alcanzado los 25MHz y 50MHz dependiendo del modelo de FPGA. Al presente
no fue implementado a nivel de silicio, o al menos no ha sido reportado en la
bibliografı́a consultada.
1 VHDL es el acrónimo que representa la combinación de VHSIC y HDL, donde VHSIC es
el acrónimo de Very High Speed Integrated Circuit y HDL es a su vez el acrónimo de Hardware Description Language. Es un lenguaje definido por el IEEE (Institute of Electrical and
Electronics Engineers) (ANSI/IEEE 1076-1993) usado por ingenieros para describir circuitos
digitales
2 La implementación de las instrucciones de acceso desalineado a memoria no fue posible,
ya que éstas estaban patentadas cuando fue diseñado
19
CAPÍTULO 2. TEORÍA Y DISEÑO
20
MIPS (en inglés Microprocessor without Interlocked Pipeline Stages) es una
ISA (en inglés instructions set architecture) RISC (En inglés reduced instruction
set computer ). Están definidas instrucciones para procesadores de 32 y 64 bits,
y diferentes versiones del set de instrucciones: MIPS I, MIPS II, MIPS III, MIPS
IV y MIPS V.
El set de instrucciones no define en ningún momento la arquitectura del
hardware del procesador, de hecho existen muchas implementaciones distintas
de los mismos, con distintas frecuencias de trabajo, con distinto tamaño de
cache, incluso con pipelines más o menos profundos.
2.2.
Comunicación entre los procesadores
Como se dijo en el capı́tulo 1, surge la necesidad de que los núcleos de los
multi-procesadores se comuniquen entre sı́ para intercambiar datos, lo cual no
sucede en procesadores de un sólo núcleo o mono-procesadores. La comunicación
entre los diferentes procesadores se realiza en general a través de la memoria
que comparten. Existen varias formas de compartir la memoria, lo que depende
de la forma en la que esta está distribuida. Pueden utilizarse buses o una redes
de comunicación, a los cuales se conectan los procesadores y la memoria. En
la figura 2.1 muestran las dos arquitecturas clásicas para la interconexión de
multi-procesadores:
En la arquitectura de shared memory o memoria compartida se observa
que existe una única memoria principal que esta conectada directamente al bus. Todos los núcleos acceden a la memoria a través del bus de
interconexión.
En la arquitectura distributed shared memory o memoria compartida distribuida se observa que la memoria no es única, sino que esta particionada
en bloques, uno por cada procesador, y a la vez se encuentra conectada a
la red de interconexión. Esto implica que dependiendo de la posición de
memoria a la que se quiera acceder un núcleo puede necesitar hacerlo a
través del bus o no y esto difiere núcleo a núcleo.
Las arquitecturas de memoria compartida tiene la particularidad de que el
tiempo de acceso a cualquier posición es independiente del núcleo que quiera
accesarla. Esto se denomina: UMA en inglés Uniform Memory Access. En una
arquitectura de memoria compartida, los núcleos están fuertemente acoplados,
lo que significa que pueden comunicarse entre sı́ a velocidades muy altas, esta
es su principal ventaja. Por otro lado la principal desventaja que presenta, son
las colisiones que se generan en el bus, por ser un bus compartido entre todos
los núcleos. Las colisiones se generan cuando varios núcleos pretendan acceder
al bus simultáneamente. Esto no es posible, ya que mientras un núcleo tiene
acceso, el resto de ellos deben esperar. Las colisiones se incrementan junto con
el aumento de núcleos conectados al bus. Por esta razón el número de núcleos
que pueden compartir un bus, tiene un lı́mite y generalmente es inferior al de
una arquitectura de memoria compartida distribuida.
En las arquitecturas de memoria distribuida compartida los tiempos de acceso a una posición de memoria depende del núcleo que quiera accesarla. Si
el núcleo debe acceder a través del bus o red de interconexión este tiempo
CAPÍTULO 2. TEORÍA Y DISEÑO
21
(a) Arquitectura Shared Memory.
(b) Arquitectura Distributed Shared Memory.
Figura 2.1: Arquitectura Shared Memory - Los procesadores tienen todos
el mismo tiempo de acceso a cualquier posición de memoria, UMA (Uniform
Memory Access). La comunicación puede ser a través de uno o varios buses, o
también a través de un switch.Arquitectura Distributed Shared Memory Los procesadores tienen distintos tiempos de acceso a distintas zonas de memoria, dependiendo si deben acceder a través del bus o no, NUMA (Non Uniform
Memory Access). La comunicación es normalmente a través de una red de comunicación.
es mayor. A este tipo de arquitecturas se las denomina NUMA en inglés Non
Uniform Memory Access. Por otra parte en las arquitecturas de memoria compartida distribuida si el sistema computacional corre un OS, en el cual se van
reprogramando las procesos que se ejecutan en cada procesador, el planificador
de procesos del sistema operativo se vuelve mucho más complejo . Al no ser uniformes los tiempos de acceso, el rendimiento al ejecutar las tareas se ve afectado
dependiendo de que núcleo sea el que ejecute el proceso, lo cual implica que el
sistema operativo no sólo debe decidir que proceso ejecutar, sino también en
cuál de los núcleos. Esta problemática también aparece en los sistemas nombrados en los sistemas asimétricos nombrados en la introducción, donde los núcleos
de procesamiento pueden ser distintos entre sı́. Para esos sistemas también debe
utilizarse un OS complejo que conozca el tipo de procesador que correrá la tarea.
Surge entonces otra clasificación para los procesadores multi-núcleo:
SMP (en inglés Symmetric MultiProcessors): Donde los núcleos son inter-
CAPÍTULO 2. TEORÍA Y DISEÑO
22
cambiables, es decir, se consiguen los mismos resultados al ejecutar una
tarea en uno o en otro núcleo de procesamiento.
AMP (en inglés Asymmetric MultiProcessors): Se obtienen distintos resultados dependiendo de la distribución de las tareas en los distintos procesadores.
En general los SMP tienen una arquitectura de memoria UMA, donde la memoria es compartida y centralizada, y se conectan a través de un bus. Este tipo
de arquitectura se utiliza para número bajo de procesadores ya que al aumentar
el número de procesadores aumentan las colisiones en el bus. Tienen la gran
ventaja de tener un acoplamiento fuerte entre los procesadores, lo cual significa
una gran velocidad de comunicación. Por el contrario las arquitecturas AMP,
cuando la asimetrı́a es producto de la distribución de la memoria, suelen tener
una arquitectura NUMA, donde la memoria es compartida y distribuida y se interconectan a través de una red de comunicación. Los procesadores están mucho
menos acoplados entre sı́, lo cual implica una disminución en la velocidad con la
que se pueden comunicar, se utiliza para aplicaciones donde los procesadores no
deben compartir una gran cantidad de datos y cuando se necesita aumentar el
ancho de banda del sistema hacia componentes externos, o sea discos rı́gidos o
la conectividad a una red, ya que cada núcleo puede tener asociado dispositivos
de entrada/salida locales. Esta arquitectura se observa en la figura 2.1(b).
Este tipo de arquitecturas son las que se utilizan generalmente, pero todo
tipo de combinaciones puede ser válida, como por ejemplo tener una única memoria compartida pero dispositivos de entrada/salida locales a los núcleos, etc.
En el diseño del plasma multi-núcleo se adopta una arquitectura tipo SMP, en el
cual las unidades de procesamiento se conectan a través de un bus, y comparten
una memoria principal, que es accedida a través del mismo. Todos los núcleos
son iguales y están basados en el procesador plasma. Se adopta esta arquitectura
por ser simple y adecuada para las aplicaciones embebidas a las que apunta el
procesador.
2.3.
Bus de interconexión
Una vez decidida la utilización de un bus para la comunicación se debe
evaluar las distintas posibilidades para su diseño. Existen diferentes estructuras
para los buses de comunicación, entre ellas las más conocidas:
Single bus en inglés o bus simple.
Switched bus en inglés o bus switcheado.
En la figura 2.2 se pueden observar estas dos posibilidades. Si se utiliza un bus
simple(figura 2.2(a)), cuando un núcleo accede al bus tiene acceso a todos los
periféricos conectados al mismo,ya sea memoria principal, o cualquier otro periférico. Mientras un núcleo tiene el acceso, el resto de los núcleo que quieran
tomar el control del mismo deberán esperar que el núcleo en cuestión lo libere.
Por el contrario un bus switcheado(figura 2.2(b)) tiene la caracterı́stica de poder
brindar acceso a dos o más núcleos a la memoria principal o a los periféricos,
siempre y cuando no haya dos de ellos tratando de acceder a una misma entidad.
CAPÍTULO 2. TEORÍA Y DISEÑO
NÚCLEO
23
NÚCLEO
NÚCLEO
NÚCLEO
MEMORIA
PRINCIPAL
(a) Bus simple
NÚCLEO
NÚCLEO
NÚCLEO
NÚCLEO
SWITCHED
BUS
BANCO 0
BANCO 1
BANCO 0
BANCO 1
MEMORIA
PRINCIPAL
(b) Bus switcheado
Figura 2.2: Bus simple y switcheado. En un bus simple, sólo un núcleo puede
tener acceso a memoria. Si dos núcleos intentan acceder a memoria, se produce
una colisión. Por el contrario, en un bus switcheado se pueden generar accesos
en paralelo entre los núcleo y los distintos bancos de memoria (si dos núcleos
intentan acceder al mismo banco de memoria, también se produce una colisión).
El resultado es un aumento en el ancho de banda entre los núcleos y la memoria.
En la figura se muestra una de las posibles conexiones entre los núcleos y los
bancos.
En algunos casos la memoria principal se puede dividir en bloques y se implementa un controlador de memoria por cada bloque. De esta manera se puede
aumentar el ancho de banda del bus, ya que permite acceso de dos o más núcleos
(hasta un máximo que depende de la cantidad de bloques o controladores de
memoria) en simultáneo, la restricción para ello es que no quieran acceder al
mismo bloque de memoria. Este bus es considerablemente más complejo, sin embargo es una de las maneras más eficientes de aumentar el rendimiento del bus.
Al aumentar en gran cantidad los núcleos de procesamiento, el bus de conexión
se vuelve un cuello de botella, limitando el rendimiento, en estos casos puede ser
de extrema utilidad cambiar el bus a un bus switcheado y dividir la memoria
principal en bloques. Al hacer uso de esta técnica se aumenta el ancho de banda
entre la memoria y los núcleos y la arquitectura sigue siendo tipo UMA, lo cuál
no es un detalle menor.
CAPÍTULO 2. TEORÍA Y DISEÑO
24
En el plasma multi-núcleo se opta por utilizar un bus simple, ya que en
principio no es necesaria otra estructura más compleja, ya que en este trabajo se pretende implementar procesadores de número relativamente bajo de
núcleos(hasta ocho). Un bus simple tiene la ventaja de permitir mensajes de
broadcast (multidestino), más adelante en este trabajo se discute el beneficio
que brinda utilizar esta estructura al algoritmo de coherencia de cache (que se
ve en la sección 3.7.3).
Al existir muchas entidades que acceden al bus, en necesario controlar los
accesos al mismo. Para nuestro caso particular estas entidades son los distintos
núcleos. Idealmente el acceso al bus deberı́a ser repartido equitativamente entre
todos los procesadores y ninguno de ellos deberı́a tener prioridad, de esta manera
que la ejecución de una tarea sea lo más independiente posible del núcleo en el
que se ejecuta. En el plasma multi-núcleo se implementa un árbitro de bus, que
será el encargado de la tarea de controlar el acceso al bus y repartirlo lo más
equitativamente posible. Su implementación y funcionamiento se describe en la
sección 3.5.
2.4.
2.4.1.
Arquitectura de la memoria
Principio de localidad
El principio de localidad surge de observar una caracterı́stica muy usual: los
programas tienden a reutilizar datos e instrucciones que han sido usadas recientemente. Una regla empı́rica es que los programas pasan un 90 % del tiempo
de ejecución con sólo un 10 % del código [2]. De este modo, a partir del pasado
reciente se puede predecir con bastante precisión que instrucciones y datos un
programa utilizará en un futuro cercano. El principio de localidad es mucho más
fuerte cuando se refiere a instrucciones o código, y no tanto al referirse a datos.
Existen dos tipos de localidad:
Localidad temporal: Es probable acceder en un futuro cercano nuevamente
a posiciones de memoria que han sido accesados recientemente.
Localidad espacial: Posiciones de memoria situadas cercanas a otras accedidas recientemente, tienden a ser accesados en un futuro cercano también.
2.4.2.
Jerarquı́a de la memoria
Al programar siempre se desea disponer de una gran cantidad de memoria
de acceso rápido. Existen memorias rápidas, que tienen un costo es elevado, y
también existen memorias lentas, que tienen un bajo costo, y por otro lado, el
principio de localidad se basa en que el código no es accedido uniformemente,
algunas porciones son utilizadas con mayor frecuencia que otras.
Lo que se busca al jerarquizar la memoria, es utilizar la memorias rápidas
y las de bajo costo en conjunto, procurando que las posiciones de memoria
que tengan mayor probabilidad de ser accedidas se encuentren ubicadas en las
memorias más rápidas. El objetivo es brindar al usuario del sistema la ilusión de
poseer un memoria tan grande como la más económica y tan rápida como la más
cara. En la figura 2.3 se pueden ver los distintos niveles tı́picos de una memoria
jerarquizada. Para aprovechar el costo y el desempeño de los distintos tipos de
CAPÍTULO 2. TEORÍA Y DISEÑO
CPU
MEMORIA
25
Velocidad Tamaño
RÁPIDA
PEQUEÑA
Costo
Ejemplo
COSTOSA
SRAM
DRAM
MEMORIA
MEMORIA
LENTA
GRANDE ECONÓMICA
DISCO
RIGIDO
MAGNÉTICO
Figura 2.3: La esctructura básica de memoria jerarquizada. Se muestra
una memoria dividida en tres niveles, y se caracteriza cada nivel de memoria
según tamaño, costo, velocidad de acceso y una tecnologı́a posible para su fabricación. A medida que la memoria se aleja del CPU, la velocidad y el costo
bajan, mientras que el tamaño aumenta. Al utilizar una memoria jerarquizada,
el programador tiene la ilusión de estar trabajando con una memoria tan grande
como la más económica y tan rápida como la más cara.
memorias se diseñan niveles de memoria que a medida que la memoria se aleja
del CPU, el tamaño aumenta, mientras que la velocidad y el costo disminuyen.
Históricamente, jerarquizar la memoria se ha vuelto más importante a medida que el desempeño de los procesadores se incrementó, ya que la velocidad
de trabajo de los procesadores aumentó más rápido que la velocidad de acceso
a la memoria principal.
La cantidad y el tamaño de los niveles de memoria puede variar en cada
caso. Una de las estructuras clásicas para estos niveles es: cacheL13 , cacheL2,
cacheL3(sólo en procesadores de alto rendimiento), memoria principal, dispositivos de entrada/salida (discos rı́gidos, memory stick, etc);
En el plasma multi-núcleo no se tienen discos rı́gidos ni otros dispositivos
de entrada/salida del estilo. Se elije implementar sólo dos niveles de memoria:
cacheL1 y memoria principal. Por la velocidad de trabajo de la FPGA, del
procesador y de la memoria externa, no vale la pena utilizar una jerarquı́a más
compleja que esta, y con estos dos niveles es suficiente.
2.4.3.
Memoria Cache
El CPU es quien realiza pedidos de datos e instrucciones en la memoria y lo
hace a la memoria más rápida que a su vez es la más cercana. Como se dijo en
la sección anterior esta memoria es también la más pequeña, lo cual la imposibilita a tener almacenados todos los datos e instrucciones existentes. Llamaremos
items a cada una de las posiciones de memoria, ya sean datos o instrucciones.
3 El
nombre cacheL1 viene del inglés cache level one.
CAPÍTULO 2. TEORÍA Y DISEÑO
26
Esta memoria entonces tiene un subconjunto de los items presentes en la memoria principal. Este tipo de memorias son llamadas memorias cache. Cuando
el CPU realiza el pedido de algún item y éste no es encontrado en una memoria
cache, traspasa el pedido al siguiente nivel de memoria hasta que se alcanza
la memoria principal. En memoria principal el item tiene que estar obligatoriamente. Cuando no se encuentra un item en cache se dice que se produjo un
miss. Por el contrario cuando si se lo encuentra se habla de un hit. Como se dijo
antes, a medida que se baja de nivel en la jerarquı́a de memoria, estas se vuelven
más rápidas. Esta es la manera en la cual se aprovecha la localidad temporal
de un item. Para aprovechar la localidad espacial de los items se utiliza otra
técnica, que es la de trabajar con bloques de memoria de tamaño mayor al de
una palabra. Llamamos una palabra a una única instrucción o dato, en el caso
del plasma están formadas por 32 bits. Si se produce un miss en una cache, en
vez de sólo copiar ese item en la cache, se aprovecha para copiar un bloque de
tamaño mayor que contenga el item en esa cache.
Al tener que copiar un nuevo bloque en una cache, inevitablemente se tiene
que escribir sobre algún otro bloque ya existente. Hay distintas maneras de
mapear la memoria principal en cache, mapeo directo, asociativo de N vı́as o
full asociativo. En algunos casos dada una dirección de memoria el bloque a
desplazar es fijo (es el caso de mapero directo), y en otras formas de mapeo
existen distintas posibilidades. En caso de existir más de un posible bloque a
reemplazar, el rendimiento depende de cual se elije. Para ello existen distintas
polı́ticas de reemplazo. Dependiendo de las polı́ticas puede variar el rendimiento
de la cache. Los tamaños de los bloques varı́an según el diseño, y también varı́an
según el nivel de la memoria. El tamaño de la cache, el tamaño de los bloques,
el mapeo de memoria en cache y la polı́tica de reemplazo afectan el desempeño
de la cache. Más adelante en esta sección se explica el mapeo de bloques de
memoria en cache y las polı́ticas de reemplazo principales.
Por otro lado, se pueden clasificar los accesos a memoria de un CPU en, datos
e instrucciones. Como se explicó en la sección 2.4.1 al analizar la ejecución de los
programas se observan tendencias de accesos a memoria, que es en lo que luego se
basa el principio de localidad. Este principio es distinto para los accesos a datos
y para los accesos a instrucciones, y es un factor significativo. Otro factor a tener
en cuenta es que al copiar un bloque de instrucciones en una memoria cache,
la probabilidad de reutilizarlas es relativamente alta. Por lo tanto se prefiere no
tener que reemplazar un bloque de instrucciones recientemente copiado por un
bloque de datos. En estas dos razones está basada la idea de dividir la memoria
cache en instrucciones y datos. Sin embargo, en ánimo de simplificar el diseño,
en este trabajo se utiliza una memoria unificada.
Mapeo de bloques y polı́ticas de reemplazo en memorias cache.
A modo de ejemplo tomemos una memoria cache con las siguientes caracterı́sticas:
Tamaño de cache: 8kB.
Tamaño de bloque: 1kB.
Ancho de la dirección: 32bits.
CAPÍTULO 2. TEORÍA Y DISEÑO
27
Figura 2.4: Distribución de bit de las direcciones de memoria. Los bits
menos significativos de la dirección son para direccionar las palabras dentro del
bloque, en este caso son diez. Luego existen una cierta cantidad de bits para
el mapeo de bloques en cache, en este caso tres, dos, unos o cero, dependiendo
de la cantidad de vı́as disponibles en la memoria cache. El resto de los bits de
la dirección, los más significativos, junto con los que se utilizan para el mapeo,
son para direccionar el bloque en memoria principal. Los bits que le siguen a
los utilizados para mapear el bloque en cache son utilizados para el tag, y la
cantidad de bits de tag puede variar.
De estas condiciones se desprende que la memoria cache tiene una capacidad
para albergar ocho bloques de memoria. Existen entonces dos maneras de mapear los bloques de memoria principal en memoria cache: directo o asociativo.
En ocasiones se habla también de un tercero que es el full asociativo, pero sigue
siendo un caso particular del asociativo. En mapeo directo la posición de ubicación de un nuevo bloque queda totalmente determinada por la dirección en
la que se encuentra en memoria principal. Para este ejemplo los últimos 10bits
direccionan una palabra dentro de algún bloque. Con los siguientes 22bits se
selecciona un bloque en memoria principal, algunos de estos bits también se
utilizan para determinar la posición del bloque en la cache, la cantidad de bits
utilizados depende del tipo de mapeado utilizado. En la figura 2.4 se pueden ver
en detalle los bits utilizados en cada caso y en la figura 2.5 un ejemplo de como
los bits de la dirección de memoria influyen en la posición que toma el bloque
en una memoria cache. Al copiar ese bloque en una memoria cache resultan los
siguientes escenarios posibles:
mapeo directo: la posición del bloque queda determinada por los 3bits (12
a 10, ver 2.4). En la figura 2.5 se ve un ejemplo de como diferentes bloques
de memoria se ubican en memoria cache. En la figura 2.6(a) se muestra
como se organizan los bloques para este caso.
asociativa de dos vı́as: la posición del bloque queda determinada en primer
lugar por los 2bits (11 a 10, ver 2.4), luego puede ubicarse en cualquiera
de los dos lugares (dos vı́as, ver figura 2.6(b)) disponibles. La vı́a en la
que queda ubicado queda determinada por la polı́tica de reemplazo.
asociativa de cuatro vı́as: la posición del bloque queda determinada en
primer lugar por un sólo bit(bit10, ver 2.4, luego puede ubicarse en cualquiera de los cuatro lugares (cuatro vı́as, ver figura 2.6(c)) disponibles.
La vı́a en la que queda ubicado queda determinada por por la polı́tica de
reemplazo.
CAPÍTULO 2. TEORÍA Y DISEÑO
28
Figura 2.5: Mapeo directo
asociativa de ocho vı́as, este caso es que a también se lo llama full asociativa, donde el bloque puede ser ubicado en cualquiera de las ocho (ocho
vı́as, ver figura 2.6(d)) posiciones de la cache, lo cual quedará determinado
por la polı́tica de reemplazo.
Para los casos donde existe más de una posición de memoria en donde ubicar
el bloque, esta posición queda determinada por las polı́ticas de reemplazo:
Aleatoria: El bloque es reemplazado de forma aleatoria y en general es la
alternativa de menor eficiencia.
FIFO: Se usa un algoritmo First In First Out FIFO (primero en entrar es
el primero en salir) para determinar qué bloque debe abandonar la caché.
Este algoritmo generalmente es poco eficiente.
Menos recientemente usado (LRU): El bloque que se copia en cache desplaza al bloque que ha pasado más tiempo en calle sin ser utilizado.
Menos frecuencias usadas (LFU): Se sustituye el bloque que ha tenido
menor frecuencia de acceso.
En el plasma multi-núcleo se utiliza una cache de mapeo directo y con un
tamaño de bloque de una palabra. Que el bloque tenga el tamaño de una única
palabra es malo en cuanto al rendimiento de la cache, pero presenta una gran
ventaja en cuanto a la simplicidad del diseño de la cache. Otra consecuencia de
esto es que se deja de aprovechar la localidad espacial, y sólo se hace uso de la
localidad temporal. Por otro lado se utiliza una polı́tica de escritura en cache y
memoria principal en cada escritura de datos. En la sección 2.4.4 se presentan
las distintas polı́ticas de escritura que existen.
CAPÍTULO 2. TEORÍA Y DISEÑO
(a) Mapeo directo.
29
(b) Mapeo asociativo de dos vı́as.
(c) Mapeo asociativo de cuatro
vı́as.
(d) Mapeo asociativo de ocho vı́as o full asociativa.
Figura 2.6: Alternativas de mapeos. En el eje vertical se ven las distintas
entradas de la cache que quedan determinadas por la dirección del bloque. En
el horizontal se muestran las vı́as disponibles para cada caso. En la memoria
de mapeo directo (2.6(a)) 3 bits seleccionan la entrada y sólo existe una único bloque a reemplazar. En asositiva de dos vı́as (2.6(b)) 2 bits seleccionan la
entrada y existen dos bloques posibles para reemplazar. En asositiva de cuatro
vı́as (2.6(c)) 1 bits seleccionan la entrada y existen cuatro bloques posibles para
reemplazar. En full asositiva (2.6(d)) la entrada es única y cualquier bloque
puede ser reemplazado para albergar al nuevo. En los casos que hay más de una
posibilidad, las polı́ticas de reemplazo se utilizan para decidir cual será reemplazado.
CAPÍTULO 2. TEORÍA Y DISEÑO
2.4.4.
30
Arquitectura de la memoria en procesadores multinúcleo
Importancia de la memoria cache
Como se explico en la sección 2.4.2 jerarquizar la memoria tiene la importante ventaja de brindar la ilusión de una memoria de gran tamaño que a la vez
es rápida y barata. Esto lo logra almacenando datos e instrucciones en memorias cache. En procesadores mono-núcleos esa es la única función de este tipo
de memoria, pero para procesadores multi-núcleo que utilizan SMP, donde los
núcleos son interconectados por un bus, es indispensable implementar bloques
de memoria cache para evitar el elevado número de colisiones de acceso al bus
que se producirı́an sin ellas.
Para explicar esto se supone un sistema de dos núcleos, en dos versiones:
una que posee solamente memoria principal y otro que además tiene al menos un nivel de memoria cache. En el caso donde los dos núcleos ejecutan un
programa que está en memoria principal, ambos núcleos intentarán acceder a
la primer posición de memoria del programa indicada por el contador de programa. Es imposible que los dos núcleos accedan simultáneamente a memoria,
entonces uno de ellos será pausado inevitablemente, mientras que el segundo
accederá a la primer posición de memoria para buscar la instrucción a ejecutar.
Una vez que el primer núcleo finalice con la utilización del bus, recién en ese
momento se cederá acceso al segundo núcleo, lo cual sucederá en el siguiente
ciclo de reloj. Pero en este nuevo ciclo, el primer procesador ya habrá finalizado
con la ejecución de la instrucción anterior y deseará acceder a la segunda instrucción. Ahora la situación será la inversa: el segundo procesador será quien
tendrá acceso al bus y a la memoria, y el primero será quien deberá esperar.
Esto sucederá constantemente, y básicamente el acceso al bus limitará a los
procesadores. Independientemente de la cantidad de núcleos que se tengan, sólo
se podrá acceder a una instrucción del código por ciclo de reloj. Entonces, cada
uno de los dos núcleos se encontrará pausado uno de cada dos ciclos de reloj, de
tener cuatro núcleos cada uno se encontrará pausado tres de cada cuatro ciclos,
etc. En el caso de no ser una instrucción lo que quiere acceder uno de los núcleos,
es exactamente lo mismo, ya que visto desde afuera, el núcleo pide acceso a una
posición de memoria, y del núcleo hacia afuera, no interesa si pretende leerla,
escribirla, si es dato o instrucción.
Para el caso en el que se tiene una memoria cache, la situación cambia completamente. En principio se consideran programas sin instrucciones de acceso a
memoria (en la ISA MIPS estas serı́an por ejemplo LW, SW, LB, SB, etc.), más
adelante se analizarán los casos en los que se accede a datos. Cuando los dos procesadores ejecuten un programa que esté en memoria principal, ambos núcleos
intentarán acceder a la primer posición de memoria del código. El primero recibirá acceso al bus y a la memoria, mientras que el segundo será pausado hasta
que el primero termine con la utilización del bus. Luego el segundo tendrá acceso a bus, pero a diferencia del caso anterior el primer núcleo ya no solicita
acceso al bus, debido a que ya posee también la siguiente posición de memoria
que necesita, porque al copiar la primera, copió todo un bloque de memoria
que incluı́a al resto de las posiciones de memoria cercanas. De esa manera se
aprovecha la localidad espacial del código.
La localidad temporal del código se aprovecha en los casos que existen bucles
CAPÍTULO 2. TEORÍA Y DISEÑO
31
de ejecución, o llamadas reiteradas a una misma función o sub-rutina, donde
se ejecutan una y otra vez las mismas instrucciones. En ocasiones particulares,
para ciertos tamaños bloque, ciertos tamaños de cache y accesos a posiciones de
memoria de cierta distancia especı́fica, se pueden producir misses consecutivos,
aumentando mucho los tiempos de acceso.
Se concluye entonces que una memoria cache es necesaria para procesadores
multi-núcleo, y que puede ser una memoria unificada de instrucciones o datos,
pero en caso de estar dividida la de instrucciones es obligatoria, mientras que la
de datos no, ya que el porcentaje de accesos a memoria para buscar instrucciones
es siempre mucho mayor al de los accesos a datos.
Polı́ticas de escritura
En general los datos no sólo son leı́dos, sino que en ocasiones también se
modifican y se vuelven a escribir en memoria. Al tener en cuenta las escrituras
de los datos otros aspectos entran en juego, al diseñar las memorias cache. Por
ejemplo, al escribir un dato, existen varias posibilidades:
Escribir el dato en memoria cache únicamente.
Escribir el dato en memoria principal y cache.
Escribir el dato únicamente en memoria principal.
La forma en la que se escribe la cache y la memoria principal toman los nombres
de polı́ticas de escritura en cache y polı́ticas de escritura en memoria principal.
Las polı́ticas de escritura en memoria principal son:
Write-Through: La memoria principal se escribe en cada instrucción de
escritura.
Write-Back : La memoria principal se escribe cuando un bloque que ha
sido modificado es desalojado de la memoria cache.
Las polı́ticas de escritura en memoria cache son:
Write-Allocate: La memoria cache se escribe en las instrucciones de escritura.
Write-No-Allocate: La memoria cache no se escribe en las instrucciones de
escritura.
la polı́tica de escritura es entonces una combinación de las polı́ticas de escritura
de memoria cache y de memoria principal:
Write-Through Write-Allocate: Poco común, pero muy simple.
Write-Through Write-No-Allocate: Poco común, también simple como la
anterior.
Write-Back Write-Allocate: La más utilizada.
Write-Back Write-No-Allocate: Sin sentido, ya que el dato no se escribe
en ninguna memoria y se perderı́a.
CAPÍTULO 2. TEORÍA Y DISEÑO
32
En general la polı́tica Write-Through es poco utilizada, ya que se debe acceder a memoria en cada escritura, sin embargo su implementación es mucho más
simple. En esta primera versión del plasma multi-núcleo se utiliza una polı́tica
Write-Through Write-Allocate, que es poco común y algo ineficiente en algunos
casos, pero que en este caso particular, al tener un bus simple y un bloque del
tamaño de una palabra se vuelve aceptable y por sobre todas las cosas mucho
más simple para implementar. En el capı́tulo de implementación (sección 3) se
explicará con mayor detalle las razones que fundamentaron su adopción.
2.4.5.
Protocolos y algoritmos de coherencia de cache
Como se explicó anteriormente, no tiene sentido tener SMP si no se implementa una memoria cache. En esta subsección se retoma el análisis sobre las
memorias cache de datos. La diferencia que estas tienen frente a las caches de
instrucciones es que las posiciones de memoria que contienen datos, no sólo se
leen, sino que también se escriben, como se mencionó anteriormente. El problema aparece cuando dos caches distintas tienen un mismo dato. Si el dato solo
es leı́do no hay ningún problema, como sucede con las instrucciones, pero si el
dato se escribe sı́. Como se dijo, el dato se puede escribir en cache, en memoria
principal o en ambas. En cualquiera de estos casos, el dato que queda albergado
en la cache del núcleo que no realiza la escritura queda desactualizado. Entonces, si éste procesador realiza una lectura del dato, leerá un valor incorrecto. La
‘coherencia de cache’se refiere justamente a este problema. En el cuadro 2.1 se
presenta un ejemplo con dos memorias cache, y con una polı́tica de escritura en
memoria write-through write-allocate Se ve que al final de las cuatro acciones
que realizan los procesadores los datos en las dos cache difieren y en este caso
el CPU1 realiza una lectura incorrecta del dato. En estos casos se dice que la
cache no es coherente, lo cual no debe suceder, porque los datos leı́dos deben
ser siempre correctos.
Que una memoria cache sea coherente implica que los datos que están almacenados en ella son válidos y que al realizar una lectura siempre se lee un
dato correcto, es decir se lee el último valor escrito en esa posición de memoria.
Para lograr la coherencia existen diferentes protocolos, que se pueden clasificar
en dos grupos:
Protocolos basados en un directorio: Existe un directorio que almacena
los estados de los bloques de memorias compartidos entre todas las caches
Acción
Estado inicial
CPU0 lee el dato
CPU1 lee el dato
CPU0 escribe
el dato
CPU1 lee el dato
Consecuencia
Valor en
Cache0
Valor en
Cache1
Miss en Cache0
Miss en Cache1
Write en Cache0 y
en memoria principal
Lee 0 cuando el
data real es 1
0
0
1
0
0
Valor real
del dato
0
0
0
1
1
0
1
Cuadro 2.1: Ejemplo de memorias cache no coherentes
CAPÍTULO 2. TEORÍA Y DISEÑO
33
del sistema. Las caches se comunican con el directorio al solicitar un dato,
el directorio es quien sabe si la cache del CPU que solicita el dato tiene
un dato actualizado o no. Es más popular en multi-procesadores de gran
escala.
Protocolos Snooping: No es centralizado. Cada cache guarda una copia del
bloque y una copia del estado del mismo. Cada memoria cache escucha a
las señales de snooping de otras memorias caches. Las señales de snooping
dan información a cada meoria cache acerca de los cambios en los estados
de los bloques. El snooping requiere un mecanismo de broadcast, que puede
ser implementado con un bus simple. En casos donde se tiene un bus
switcheado no es posible realizar broadcast. Muchas veces se utiliza un bus
adicional, exclusivamente para implementar los algoritmos de coherencia.
Existen varios algoritmos para mantener la coherencia entre ellos los más
populares son: MSI, MESI, MOSI, MOESI [2] [7]. Este tipo de protocolo
está presente en la mayoria de los procesadores actuales.
En el plasma multi-núcleo se utiliza un protocolo de snooping, por ser más
simple y adecuado para la arquitectura. Independientemente del protocolo que
se utilice para mantener la coherencia de cache también se debe de tener en
cuenta el algoritmo que se utiliza. En el protocolo de snooping, como se dijo
antes, se debe de enviar información sobre las acciones que se realizan sobre los
bloques a cada una de las caches. Para ellos se necesitan señales extras en el
bus local del procesador. En el capı́tulo 3 se detalla el protocolo y el algoritmo
implementado y que señales se utilizan para lograr la coherencia.
2.5.
Manejo de interrupciones
Otro factor a tener en cuenta al pasar de sistemas de un sólo núcleo a SMP,
es el manejo de interrupciones. En procesadores mono-núcleo la interrupción de
un núcleo depende de los siguientes elementos:
El estado de cada una de las fuentes de interrupción.
La máscara de interrupciones.
Si las mismas están habilitadas o no.
Básicamente se realiza una operación and bit a bit entre la máscara y el estado
de interrupciones, y si este resultado es distinto de cero y las mismas están
habilitadas, el procesador es derivado a alguna tarea que sea capaz de manejar
la excepción.
En un procesador multi-núcleo las posibilidades aumentan ya que existen
varios núcleos que pueden ser interrumpidos. Cada uno de los núcleos a su vez
puede habilitar y deshabiltar las interrupciones por separado. Algunos sistemas
implementan también las interrupciones inter-procesadores. Esto no será incluido en este trabajo de tesis.
Para el plasma multi-núcleo se quiere un manejador de interrupciones que
distribuya los pedidos de interrupciones en forma pareja entre todos los procesadores, de modo tal que no sea siempre el mismo núcleo el encargado de
manejarlas, y ası́ lograr que todos lo núcleos se comporten igual. No se tiene en
CAPÍTULO 2. TEORÍA Y DISEÑO
34
cuenta si el procesador tiene bloqueadas o no las interrupciones, esto podrı́a implementarse en alguna versión futura del plasma de modo de mejorar la latencia
que tiene una interrupción en ser procesada. La latencia de la interrupción es
el tiempo que tarda desde que da una excepción hasta que se ejecuta la tarea
indicada.
2.6.
Operaciones atómicas
Una operación atómica hace referencia a la lectura-modificación-escritura
de una posición de memoria. En ningún caso es posible resolverlo en una única
instrucción, por lo tanto se implementa mediante un conjunto de instrucciones.
Este conjunto de instrucciones debe de ser ejecutado en forma secuencial y sin
ser interrumpidas. El efecto buscado con estas instrucciones puede ser afectado
si esto no se cumple. Se utiliza la exclusión mutua, que es un mecanismo para
evitar que dos o mas procesadores entren en una sección crı́tica al mismo tiempo.
La exclusión mutua se logra a su vez utilizando secciones crı́ticas. Una sección
crı́tica es una sección de código en la cual el procesador tiene acceso exclusivo a
los recursos compartidos y bajo ningún punto de vista debe ser interrumpido. Si
proceso se encuentra en una sección crı́tica y un segundo proceso quiere entrar
a una sección crı́tica, este último proceso es bloqueado hasta que el primero
sale de la sección crı́tica, ver figura 2.7. Ası́ se evita que haya dos procesos
utilizando recursos compartidos. Se dice entonces que los recursos compartidos
son protegidos mediante secciones crı́ticas, y ası́ se logra la correcta utilización
de los recursos.
Lograr la exclusión mutua entre procesos es simple en procesadores mononúcleo. Basta con deshabilitar las interrupciones. De esta manera el proceso que
corre en el CPU en ese momento no puede ser interrumpido, por lo tanto tampoco puede ser reemplazado, y al no existir otro procesador corriendo otro proceso
en paralelo, éste es el único proceso que corre y por lo tanto tiene acceso exclusivo a los recursos. Para salir de una sección crı́tica basta con restaurar el estado
anterior de las interrupciones. En procesadores multi-núcleo la implementación
no es trivial. Para entrar en una sección crı́tica, un proceso debe asegurarse que
ningún otro proceso se encuentra en una sección crı́tica. Lo primero que hace
un proceso que quiere entrar en una sección crı́tica es deshabilitar las interrupciones del núcleo en el que corre de manera que evite ser reemplazado por otro
proceso. Luego hay que asegurar que ningún otro proceso, que esté corriendo en
otro núcleo se encuentre en una sección crı́tica. Si otro proceso se encuentra en
una sección crı́tica, el nuevo proceso que quiere entrar se bloquea. La manera
que se tiene de hacer esto es utilizando los llamados locks, que es algún tipo de
registro o variable compartida entre los procesadores. Se dice que un proceso
tiene un lock cuando es el que tiene el permiso para acceder a los recursos, es
decir se encuentra en una sección crı́tica. En este caso se puede hablar de proceso o núcleo indistintamente, ya que al deshabilitar las interrupciones el proceso
queda ligado al núcleo. Al salir de una sección crı́tica el proceso libera el lock.
La manera de implementar un lock es guardando su estado en un registro.
Un ejemplo serı́a cuando ningún proceso tiene el lock, el registro tiene el valor
numérico menos uno almacenado. Cuando algún proceso se adueña del lock el
registro guarda el número del CPU que corre el proceso que tiene el lock. De
esta manera un proceso antes de entrar en una sección critica se asegura que el
CAPÍTULO 2. TEORÍA Y DISEÑO
35
TIEMPO
PROCESO A
SECCIÓN CRÍTICA de A
B está
bloqueada
PROCESO B
T0
T1
SECCIÓN CRÍTICA de B
T2
T3
T0: A entra a una sección critica.
T1: B intenta entrar a una sección crítica, pero no puede y se bloquea.
T2: A deja la sección crítica y B entra a una sección crítica.
T3: B deja la sección crítica.
Figura 2.7: Exclusión mutua, evita que dos procesos ingresen en una sección
crı́tica simultáneamente.
registro del lock este en menos uno. Le asigna al registro el valor del CPU donde
esta corriendo y entra en la sección crı́tica. Cuando algún otro proceso quiera
entrar en una sección crı́tica y revise el valor del registro, el mismo no será cero
entonces no podrá entrar en la sección crı́tica y deberá volver a intentar nuevamente hasta que se libere el lock (o lo que es lo mismo que el registro del lock
valga cero) (ver figura 2.8). Este proceso se lo llama spinLock [10]. El proceso de
obtención de un lock para entrar en una sección crı́tica se muestra en la figura
2.8. Se ve que antes de empezar a competir por el lock, los procesos guardan
el estado de las interrupciones (habilitado/deshabilitado) del CPU donde están
corriendo, para poder restaurarlos al finalizar con la sección crı́tica. Una caracterı́stica que deben tener las secciones crı́ticas protegidas por locks es ser lo más
acotadas que sea posible, ya que al entrar a una sección crı́tica bloquean no sólo
a otros procesos que quieran entrar en estas secciones, sino también al CPU en
el que corren, ya que no es posible cambiar interrumpir al CPU para cambiar el
proceso que esta corriendo en él. Este tipo de implementación lleva el nombre
de mutual exclusion with busy waiting [10]. Existen otros artilugios utilizados
por los sistemas operativos para proteger recursos compartidos y que a la vez
pueden ser accedidos por un CPU por un largo perı́odo sin bloquear a los otros
CPUs, como son lo Mutex, Semaphores, entre otros [10].
Presentado de esta manera el problema, parecerı́a que la exclusión mutua
funciona sin inconvenientes, pero mediante el ejemplo del cuadro 2.2 se demuestra que no. En el ejemplo se toma un sistema con dos núcleos. Al final del quinto
ciclo los dos procesos (el que corre en el CPU0 y el que corre en el CPU1) se encuentran simultáneamente en secciones crı́ticas, lo cual es un estado indeseado,
ya que no cumple con la exclusión mutua. El problema al querer implementar
un lock se muestra en la figura 2.9: para querer implementar un lock es necesario
realizar operaciones atómicas, y para realizar operaciones atómicas es necesario
tener un lock.
CAPÍTULO 2. TEORÍA Y DISEÑO
36
Figura 2.8: Exclusión mutua a través de la utilización de un spinLock.
El diagrama en bloques de arriba muestra el proceso para obtener el lock, entrar
a una sección crı́tica, hacer uso de algún recurso compartido, salir de la sección
crı́tica y liberar el lock. Si un proceso ya posee el lock es imposible que otro lo
obtenga al mismo tiempo. El proceso que quiera obtenerlo quedará en un bucle,
hasta que el lock sea liberado, y pueda obtenerlo. El lock es quien permite a los
procesos acceder o no a las secciones crı́ticas.
CAPÍTULO 2. TEORÍA Y DISEÑO
37
Ciclo
CPU0
CPU1
0
Quiere entrar
a una sección
crı́tica
Lee registro del lock
Compara que sea -1
Escribe el resgistro
del lock con 0
Entra en la sección
crı́tica
Quiere entrar
a una sección
crı́tica
Sin acceso al bus
Lee registro del lock
Compara que sea -1
1
2
3
4
5
Escribe el resgistro
del lock con 1
Entra en la sección
crı́tica
Registro
del lock
−1
−1
−1
−1 → 0
0→1
1
Cuadro 2.2: Ejemplo con dos núcleos de la problemática que aparece al querer
implementar un lock en forma directa.
Las siguientes son las operaciones atómicas que deben realizarse para obtener
un lock:
Lectura del registro de estado del lock.
Evaluar si el valor corresponde al de lock liberado.
Si se encuentra liberado guardar un nuevo estado en el mismo, de lo contrario no hacer nada.
En lenguaje assembler de MIPS I, esto lleva al menos tres instrucciones y resulta
imposible asegurar su atomicidad. Si se considera que $2 apunta al registro del
lock y que $3 posee el número del CPU que ejecuta el proceso, entonces las
instrucciones serı́an:
1
2
3
4
5
$spinLock :
lw
$1 , 0($2)
bnez $ 1 , $ s p i n L o c k
nop
#Se e j e c u t a siempre , s i n i m p o r t a r s i e l s a l t o s e
toma o no .
sw
$3 , 0($2)
O con una versión equivalente: Si se agrega la condición de que $1 sea distinto de
cero se puede reescribir el código de la siguiente manera ocupe tres instrucciones:
1
2
3
4
5
li
$1 , 0
$spinLock :
bnez $ 1 , $ s p i n L o c k
lw
$1 , 0($2)
#Se e j e c u t a siempre , s i n i m p o r t a r s i e l s a l t o
s e toma o no .
sw
$3 , 0($2)
Durante el perı́odo de tiempo que existe entre las instrucciones dos y cinco del
primer caso, o tres y cinco del segundo ningún otro núcleo deberı́a de realizar
una lectura o escritura del registro del lock (apuntado por 0($2)). Al resultar imposible comprobar esto se puede caer en el problema mostrado en el cuadro 2.2.
Para evitar este problema, en MIPS II se han definido dos nuevas instrucciones
especialmente diseñadas para realizar éstas operaciones atómicamente:
CAPÍTULO 2. TEORÍA Y DISEÑO
38
Figura 2.9: Problemática de las operaciones atómicas.
LL - Load Link :
SC - Store Conditional :
El código cambiarı́a por:
1
2
3
4
5
$spinLock :
ll
$1 , 0($2)
bnez $ 1 , $ s p i n L o c k
nop
#Se e j e c u t a siempre , s i n i m p o r t a r s i e l s a l t o s e
toma o no .
sc
$3 , 0($2)
Y se agregarı́an las siguiente instrucciones para comprobar que las operaciones
LL y SC sobre la posición de memoria apuntada por 0($2) hayan sido atómicas,
y en caso de no ser atómicas se vuelve a intentar:
6
7
bez
$3 , $spinLock
#S i l a s i n s t r u c c i o n e s no f u e r o n a t o m i c a s
e n t r e l l y s c e n t o n c e s $3 e s i g u a l a 0
nop
#Se e j e c u t a siempre , s i n i m p o r t a r s i e l s a l t o s e
toma o no .
Como se comenta en el código si las instrucciones entre LL y SC no fueron
atómicas, entonces $3 termina con valor cero después del SC, sino con valor
uno.
Al ejecutar LL, un flag es seteado y se mantiene ası́ a menos que:
Otro CPU realice algún Store en la misma dirección fı́sica de memoria
utilizada en el LL.
Que ocurra una excepción entre LL y SC en el CPU que las está ejecutando.
El funcionamiento de estas instrucciones se define en la ISA de MIPS [6]. La
ISA es más especı́fica en cuanto a la explicación y pone como condición que
CAPÍTULO 2. TEORÍA Y DISEÑO
39
la dirección debe ser cacheable, y los stores pueden ser realizados por otras
entidades (no sólo CPUs).
Los núcleos del plasma no soportan las instrucciones de MIPS II por eso es
que se debe buscar otra solución a este problema. Algunos autores proponen
soluciones por software a este problema como en [10]. Éstas soluciones son poco
eficientes y en la bibliografı́a sólo se muestran ejemplos funcionales para dos
núcleos. En el apéndice A se muestra el código para una implementación en
software de spinLock.
En el procesador plasma multi-núcleo no se utilizarán las soluciones de software, ya que no se han estudiado en profundidad y no se puede asegurar nada
acerca de su rendimiento ni que funcionen sin caer en un deadlock, término utilizado para identificar una situación en la cual dos CPU (o más) están esperándose mutuamente a que terminen alguna operación, y ninguno puede hacerlo. En
cambio se brinda soporte extra por hardware para las operaciones atómicas,
la implementación es muy simple y se ve en la sección 3. Vale aclarar que en
caso de que no se quiera utilizar simplemente se puede implementar una de las
soluciones propuestas por software sin ningún problema. Otro soporte que debe
brindar el hardware para sistemas multi-núcleo es el indice del CPU que corre el
proceso. En el plasma multi-núcleo se implementa en un registro fijo por núcleo
que almacena este valor.
2.7.
Caracterización a priori del procesador
Para finalizar este capı́tulo se presenta un resumen de las decisiones de diseño
tomadas.
En particular implementaremos el diseño en un kit Nexys2 de Digilent que
posee una Spartan-3E-1200, el diseño cumplirá con las siguientes caracterı́sticas:
Multi-procesador genérico: Se pueden implementar N núcleos.
Procesadores:
• Escalar: O sea un sólo Issue Slot.
• Pipeline de 2 o 3 etapas a elección.
Frecuencia de trabajo: 25M Hz.
Cache:
• 512kB de tamaño.
• Tamaño de bloque de 32bits (una palabra).
• Compartida para instrucciones y datos.
• Polı́ticas de escritura Write-Through Write-Allocate.
• Mapeo directo.
• Protocolo de snooping para la coherencia de cache.
Memoria RAM: 16M B de memoria para instrucciones y datos.
Memoria interna en cada procesador: 1KB
Conexión serie UART.
CAPÍTULO 2. TEORÍA Y DISEÑO
40
GPIO:
• 1 puerto de salida de 32bits.
• 1 puerto de entrada de 32bits.
Interrupciones: Las interrupciones son emascarables globalmente, y se pueden habilitar en cada núcleo por separado. Las fuentes de interrupciones
son:
• Interrupción por nivel en uno de los pines del puerto de entrada.
• Temporizador: cada 10ms aproximadamente.
• UART: Nuevos datos recibidos.
• UART: Disponible para el envı́o de datos.
1 contador de 32 bits.
2 registros de propósito general, para comunicación de los procesadores.
Soporte adicional de hardware:
• Para identificación del número núcleo.
• Para implementación de locks.
Capı́tulo 3
Implementación y
resultados obtenidos
En este capı́tulo se hace primero una breve descripción de las herramientas
utilizadas y luego se detalla la forma en la que se implementó cada bloque del
plasma multi-núcleo. Se presenta luego la implementación de la capa superior y
se avanza hacia las capas inferiores. De esta manera queda ordenado el trabajo
y se comprende claramente la arquitectura diseñada.
3.1.
Herramientas utilizadas
El diseño total del sistema se realizó en un entorno de un sistema operativo
linux, que es un sistema operativo libre y gratuito. Las principales herramientas
utilizadas para la implementación del procesador fueron las siguientes:
Eclipse+Sigasi: Eclipse es una interfaz de desarrollo genérica. El plugin
Sigasi brinda soporte para el desarrollo de código de VHDL y Verilog.
Kate: Es un editor de texto que se utilizó en algunos casos que resultaba
más cómodo que el eclipse.
GHDL: Es un compilador y simulador del lenguaje VHDL.
gtkwave: Es un visualizador de señales, que permite ver resultado de las
simulaciones realizadas mediante GHDL.
IseWebPack: Es una herramienta de Xilinx gratuita que se utilizó para
sintetizar los circuitos y generar los archivos necesarios para programar la
FPGA. La herramienta brinda muchas mas prestaciones, como compilar
y simular VHDL, visualizador de señales y interfaz para programar las
FPGAs, pero sólo se utilizó para la sı́ntesis de los circuitos.
DigilentAdept: Es un set de herramientas utilizadas para comunicarse con
los kits de desarrollo de Digilent. Brinda la interfaz para programar las
FPGAs de estos kits.
41
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
42
Vale destacar que todas las herramientas y software utilizada para su realización son de carácter libre o bien gratuito.
3.2.
Estructura del procesador multi-núcleo
La estructura general del procesador se ve en la figura 3.1. Se ven cuatro
capas principales:
1. La capa superior, que incluye el procesador plasma multi-core y el controlador de memoria. Este último es dependiente del kit en el que se implementa.
2. La arquitectura interna del procesador multi-núcleo, que incluye entre
otras entidades al árbitro del bus y a los núcleos.
3. La arquitectura interna del núcleo, que incluye a los núcleos de procesamiento del plasma original y también toda la arquitectura de la memoria
cache.
4. La capa inferior para nosotros serán las unidades de procesamiento del
plasma original.
3.3.
Controlador de memoria
Como se dijo anteriormente, se comienza por presentar el controlador de
memoria que es la capa superior del sistema. El controlador de memoria funciona
como interfaz entre el plasma y la memoria externa (ver figura 3.2). La memoria
externa es fabricada por Micron y el modelo es MT45W8MW16BGX. Su función
es adaptar las diferencias que pueda haber en ancho de palabras, ancho de
direcciones, tiempos de acceso, etc.
El plasma posee dos puertos uno de entrada de datos (dataR) y otro de
salida datos (dataW), cada uno con un ancho de 32bits y está diseñado para
una memoria asincrónica. Por otro lado la memoria posee un único puerto de
entrada/salida de 16bits.
3.3.1.
Descripción
El controlador de memoria es una simple máquina de estados, que arranca
en un estado de inicialización. Como la memoria viene configurada por defecto
para operar en modo asincrónico, lo único que se hace en este estado es esperar
una cierta cantidad de ciclos hasta que la memoria se encuentre operativa. El
número de ciclos que debe esperar es parametrizable, de esta manera se puede
cambiar fácilmente la frecuencia de trabajo del mismo. Una vez finalizada la
inicialización se pasa a un estado ocioso a la espera de alguna solicitud de
escritura o lectura. Por cada solicitud de escritura/lectura del CPU se realizan
dos escrituras/lecturas en memoria, esto se debe a la diferencia del ancho de
palabra, la del CPU es de 32bits mientras que la memoria 16bits. Nuevamente
los ciclos de espera para una lectura o escritura son parametrizables.
El controlador de memoria se puede hacer funcionar a una frecuencia igual o
mayor a la del CPU, y de esta manera se podrı́an obtener pequeñas mejoras en
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
43
Figura 3.1: Estructura jerárquica del plasma multi-núcleo.
los tiempos de acceso. En esta implementación y haciendo trabajar al controlador a 25M Hz, igual que el procesador, se obtiene una lectura de 32bits en ocho
ciclos y una escritura de 32bits en diez ciclos. Esto nos brinda tiempos de escritura de 400ns y en caso de haber un miss en cache la penalidad será de 320ns.
En la figura 3.3 se pueden ver simulaciones de las operaciones de escritura y
lectura.
3.3.2.
Puertos de la entidad
clk i (entrada): Para sincronizar.
reset (entrada)i: Para reiniciar la unidad.
Interfaz plasma multi-núcleo:
• Salida 30bits de dirección: El procesador direcciona con 30 bits palabras de 32 bits (o 4 bytes).
• Salida 32bits de datos a escribir: Los datos que el procesador escribe
en memoria.
• Entrada 32bits de datos a leer: Los datos que el procesador lee desde
la memoria.
32 bits-dataW
32 bits-dataR
4 bits-byteWE
busy_o
request_i
reset_i
clk_i
44
23bits-dirección
16 bits-dataIO
memCE_o
memWE_o
memLB_o
memUB_o
memOE_o
memClk_o
memAdV_o
memCRE_o
memWait_i
Memoria ram externa
Plasma multi-núcleo
22bits-dirección
Controlador de memoria
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
Figura 3.2: Controlador de memoria para el plasma multi núcleo. El mismo
depende del kit en el que se implementa. Las señales en van del controlador a
la memoria externa y las verdes del controlador al plasma multi-núcleo
• Salida 4bits de habilitación por byte para la escritura:
• Salida solicitud de memoria: Indica cuando las señales son válidas,
es decir cuando comenzar una escritura o lectura de memoria.
• Entrada memoria ocupada: Esta señal indica al plasma cuando se ha
finalizado el acceso a memoria solicitado. Si la solicitud es de escritura
entonces cuando esta señal lo indique podrán ser leı́dos los datos y no
antes. Un cero lógico indica que los datos son válidos, un uno lógico
que el controlador sigue trabajando y por lo tanto que los datos no
son válidos.
Interfaz de la memoria:
• Entrada 23 bits de dirección: Para direccionar las 223 posiciones de
memoria de 16 bits, el procesador direcciona con 22 bits efectivos
palabras de 32 bits (Memoria: 2(23bitsdireccion) × 16bits de datos =
128M bits - CPU: 2(22bitsdireccion) × 32bits de datos = 128M bits)).
• Entrada/Salida 16bits de datos a escribir/leer: La memoria utiliza un
única puerto de 16bits para escribir y leer datos.
• Entrada memClk (Clock ): Se utiliza para los modos sincrónicos, en
este caso tiene el valor constante cero.
• Entrada memAdV (Address Valid ): Se utiliza para los modos sincrónicos, en este caso tiene el valor constante cero.
• Entrada memCRE (Configuration Register Enable): Se utiliza para
configurar el modo de funcionamiento de la memoria, en este caso
tiene valor constante cero, ya que la memoria viene configurada por
defecto para operar en modo asincrónico.
• Entrada memCE (Chip enable): Se utiliza para habilitar o deshabilitar la memoria. En este caso tiene un valor constante cero, lo cual
indica que la memoria está siempre habilitada.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
45
(a) Operación de lectura.
(b) Operación de escritura.
Figura 3.3: Operaciones de escritura y lectura. En la figura 3.3(a) se visualizan las señales del controlador de memoria al realizar una operación de lectura,
se ve como la lectura de 32bits del CPU requiere dos lecturas de memoria de
16bits. El dato leı́do (dataR o) cambia primero los 16bits menos significativos
y luego el resto. Una vez que se tiene el dato válido la señal de busy o toma el
valor cero. En la figura 3.3(b) se visualizan las señales del controlador de memoria al realizar una operación de escritura. En este caso se realiza la escritura del
byte más significativo de la palabra de 32bits (bits31 1 24). La primera de las
dos escrituras en memoria es en los bits15 a 0 y la segunda en los bits31 a 16.
Como se escribe el byte más significativo sólo se habilita la señal memUB o (se
habilita la escritura dejando la señal cero) en la segunda escritura en memoria.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
46
• Entrada memOE (Output Enable): Habilita la salida de la memoria.
Tendra valor cero cuando se realicen lectura de datos y uno al escribir
la memoria.
• Entrada memWE (Write Enable): Habilita la escritura de la memoria.
• Entrada memLB (Lower Byte enable): Habilita la escritura del byte
bajo (bits7 a 0) de la palabra (de 16bits) que se escribe.
• Entrada memUB (Upper Byte enable): Habilita la escritura del byte
alto (bits15 a 8) de la palabra (de 16bits) que se escribe.
• Salida memWait (Wait): Provee información relevante del estado de
la memoria al operar en modo burst, en este caso su valor será ignorado.
3.4.
Plasma multi-núcleo
Siguiendo con la descripción jerárquica de las entidades, aquı́ se aborda el
plasma multi-núcleo. Esta es la entidad que alberga a todo el resto del diseño.
A continuación se muestra el detalle de su arquitectura y luego se realiza un
listado de los puertos de entrada y salda de la entidad (ver figuras 3.4 y 3.5).
3.4.1.
Descripción
Se listan a continuación los distintos bloques que presenta el plasma multinúcleo en su arquitectura interna. Aquellas simples serán descriptas directamente en esta sección otras más complejas se detallarán en secciones posteriores.
1. N núcleos que se quieran instanciar: La arquitectura de cada núcleo se
explica en la sección 3.7.
2. Árbitro del bus: Su arquitectura se explica en la sección 3.5.
clk_i
reset_i
addres_o (30 bits)
we_o (4 bits)
dataW_o
uart_RX_i
Entrada
uart_TX_o
/
Salida
GPIO0_o (32 bits)
Plasma
multi-núcleo
dataR_i
Al controlador
de
memoria
memBusy_i
busBusy_i
GPIOA_i (32 bits)
Figura 3.4: Puertos de entrada y salida del plasma multi-núcleo. Aparte
del reset i y el clk i tenemos dos tipos de pines en el procesador: aquellos
que son de la interfaz con la memoria y los de entra/salida hacia el usuario.
Señales (de izq. a der.):
- busSnoop_i
- memRequest_o
- cpuPause_i
- busRequest_o
- busAccessEnable_i
- busBusy_i
- irq_i
- lockRequest_o
Señales hacia el
controlador de
memoria
externo:
- busDataR_s
- busDataW_s
- busAddress_s
- busWe_s
- memRequest_s
Señales (de izq. a der.):
- cpuDataR_s
- cpuDataW_s
- cpuAddress_s
- cpuWe_s
Las señales de clk_i y reset_i
van a todos los bloques
Árbitro de
bus
Núcleo
Núcleo
Manejador de
interrupciones
Multiplexor
memRequest_s
cpuPause_s
busSnoop_s
Generador de señales
irqStatus_s
irqMask_s
busValid_s
Soporte de HW
para locks
Núcleo
busAddress_s
busWe_s
memRequest_s
Regs. de intercom.
UART read
UART write
Máscara de IRQ
Estado de IRQ
Contador
GPIO (E/S)
Componentes
varios
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
47
Figura 3.5: Arquitectura del plasma multi-núcleo, agrupa a todo el resto
de las entidades del procesador.
memRequest_s
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
48
3. Manejador de interrupciones: Su arquitectura se explica en la sección 3.6.
4. Multiplexor del bus: Este sistema recibe todas las señales que pueden ser
leı́das por el CPU y también la señal de escritura proveniente del CPU.
Mediante las señales de dirección y de habilitación de escritura por byte
(cpuAddress s y cpuWE s), rutea las señales a los destinos correspondientes.
El dato de lectura del núcleo (la señal cpuDataR s) toma valores de alguna
de las siguientes fuentes:
memoria interna
la memoria externa (directo de memoria externa si se produce un
miss o dato proveniente de cache durante un hit).
el puerto de lectura de UART (datos recibidos).
el puerto de entradas de propósito general (GPIO puerto A).
el puerto de salida (GPIO puerto 0).
los registros de comunicación inter-procesador, son registros de propósito general.
el registro de estado de interrupciones.
el registro de máscara de interrupciones.
el registro del lock, se .
el registro del contador.
El CPU por su parte la señal de escritura del CPU (señaL cpuDataW s)
puede ser enviada a varios destinos:
los registros de intercomunicación.
el puerto de salida de datos de la UART (datos para ser transmitidos).
el puerto de salidas de propósito general (GPIO puerto 0).
el registro de máscara de interrupción.
la memoria interna.
la memoria externa (se escribe memoria externa y cache al mismo
tiempo, por la polı́tica de escritura write-allocate).
Todos los registros especiales son mapeados en direcciones de memoria.
En la sección 3.8 se detalla la distribución de las direcciones de memoria.
5. Manejador del lock : Es una porción de hardware para el soporte de locks, el
resto del hardware necesario para su implementación se encuentra en cada
uno de los núcleo. Por su parte, cada núcleo envı́a una señal de solicitud a
esta entidad, quien las chequea y entrega el lock al núcleo que corresponda
(únicamente a uno). La forma que tiene de entregar el lock a uno u otro
núcleo, es asignando el valor correspondiente a ese núcleo en el registro
del lock 1 .
1 El valor correspondiente al núcleo i es 2i , un valor nulo en el registro del lock indica que
ningún núcleo lo posee.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
49
Esta entidad analiza las solicitudes de los núcleos, una en cada ciclo de
reloj. Las solicitudes son realizadas por los núcleos a través de la señal de
solicitud(lockRequest s). Si el núcleo que es chequeado solicita el lock,
esta entidad se lo entrega. En cambio si no hay una solicitud, se procede
a chequear la señal de solicitud del siguiente núcleo. Cuando el lock ya se
encuentra asignado a un núcleo, la entidad deja de chequear las solicitudes
del resto de los núcleos y espera a que el núcleo que posee el lock lo libere.
El mecanismo que implementan los núcleos para liberar el lock es borrar
la solicitud (señal que recibe esta entidad). Una vez liberado el lock es
posible seguir evaluando las siguientes solicitudes. La implementación en
VHDL se muestra a continuación:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
u2 l o c k H a n d l e r : process ( c l k i , r e s e t i ,
plasmaCoreLockRequest s )
variable checkCpu v : i n t e g e r ;
begin
i f r e s e t i = ’ 1 ’ then
l o c k R e g i s t e r s <= ZERO( 3 1 downto 0 ) ;
checkCpu v : = 0 ;
e l s i f r i s i n g edge ( c l k i ) then
i f plasmaCoreLockRequest s ( checkCpu v ) = ’ 1 ’ then
l o c k R e g i s t e r s ( checkCpu v ) <= ’ 1 ’ ;
else
l o c k R e g i s t e r s <= X” 00000000 ” ;
checkCpu v := ( checkCpu v + 1 ) mod numberOfCores ;
end i f ;
end i f ;
end process ;
Esta implementación soporta hasta treinta y dos núcleos y a la vez debe ser
igual a alguna potencia de dos debido a las operaciones en módulo (lı́nea
12 del código VHDL). Otra deficiencia del diseño es que al controlar uno
a uno los cores, se puede tener una latencia entre la solicitud del lock y la
obtención del mismo de hasta N ciclos de reloj en el peor de los casos, con
N = numero de nucleos. Esto último no es de gran importancia ya que en
general el tiempo que tarda el CPU en chequear el registro del lock (para
saber si lo obtuvo o no), es mayor que esta latencia, e inclusive en el peor
de los casos tiene mejor rendimiento que la implementación por software
propuesta en el sistema operativo del plasma. Esta implementación es
simple, requiere poco hardware, brinda un reparto equitativo de los locks
entre los núcleos, se desempeña mejor que la implementación por software
y se asegura que no habrá deadlocks entre los núcleos que compitan por
el recurso.
6. Generador de señales: No es una entidad real, es una forma de agrupar
tres procesos presentes en el plasma multi-núcleo:
a) busSnoop s: Es una señal necesaria para la impementación del algoritmo de snooping para la coherencia de cache. La señal es repartida
a todos los núcleos, en la sección 3.7.3 se explica como cada núcleo
la utiliza esta señal para mantener la coherencia de cache. La señal
toma un valor lógico alto cada vez que alguno de los núcleos realiza
una escritura en memoria, y se mantiene en ese valor durante un solo
ciclo de reloj. En las escrituras en memoria es en el único momento
en el que una posición de memoria puede ser modificada.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
50
b) memRequest s: Es la señal que se envı́a al controlador de memoria
para solicitar una lectura escritura de un dato. Esta señal toma el
valor lógico uno cuando el núcleo que tiene accesos al bus realiza una
solicitud de memoria, el resto de las solicitudes de los núcleos sin
acceso al bus son ignoradas.
c) corePause s: Esta es la señal de pausa que recibe cada uno de los
núcleos. Un núcleo es pausado si:
Si el núcleo que posee acceso al bus quiere acceder a memoria y
el controlador indica que esta ocupada.
Si se intenta escribir en el registro de la UART y ésta se encuentra
ocupada enviando un dato previo.
El núcleo internamente puede pausarse a sı́ mismo, esto sucede
por ejemplo en los casos donde no consiguen el acceso al bus.
7. Componentes varios: Al igual que en el item anterior, este entidad no
existe realmente, es una manera de agrupar varios componentes que están
relacionados:
a) UART: Es una controlador de UART estándar, que se accede a través
de dos registros, uno para leer datos recibidos y otro para enviar
datos. La velocidad del mismo es configurable durante el proceso de
sı́ntesis, pero luego queda fija. Los datos son de 8 bits y los datos se
envı́an y reciben sin bit de paridad.
b) Regitros para las solicitudes de interrupción (IRQ del inglés Interrupt
ReQuest): posee dos registros, para leer el estado de las interrupciones
y la máscara de interrupción. El registro de máscara también puede
ser escrito.
c) Registro del contador: Tiene 32bits y se incrementa en cada ciclo. Sólo
es posible leerlo, para saber la cantidad de ciclos que transcurrieron
desde que se alimentó el circuito. El bit 18 del contador funciona
como fuente de interrupción del procesador.
d ) Registros del GPIO: Son dos registros que representan los puertos
de entrada y salida del procesador. El registro del puerto de entrada
(32bits) puede ser leı́do. El registro del puerto de salida (32bits) puede
ser escrito o leı́do. Las escrituras en el registro del puerto de salida
se pueden realizar son bit a bit.
e) Registros para comunicación inter-procesadores: Dos registros de propósito general que se comparten entre todos los procesadores. Brindan
una forma de comunicación entre los núcleos sin necesidad de recurrir
a la memoria externa. Son útiles a la hora de ejecutar programas de
de inicialización de memoria o durante el boot de los procesadores
antes de que se inicie el sistema operativo.
3.4.2.
Puertos de la entidad
clk i (entrada): Para sincronizar.
reset (entrada)i: Para reiniciar la unidad.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
51
Interfaz con la memoria externa: todas las señales se envı́an hacia el controlador de memoria o se reciben desde el mismo.
• address o (salida 30 bits): Para direccionar la memoria externa. Esta
señal se envı́a al controlador de memorial.
• we o (salida 4 bits): para indicar escritura selectiva de los 4bytes de
las palabras de 32bits.
• dataW o (salida 32 bits): dato de escritura en memoria principal a
través del controlador de memoria.
• dataR i (entrada 32 bits): dato de lectura provenientes de la memoria
principal a través del controlador.
• memBusy i (entrada): señal que indica que la memoria esta ocupada,
por lo tanto los datos de lectura que entrega no son válidos.
• memRequest o (salida): señal para solicitar un acceso a memoria, las
señales dataW o y address o deben tener valores válidos.
Entrada/Salida:
• uart TX o (salida): señal de transmisión de datos de la UART.
• uart RX i (entrada): señal de recepción de datos de la UART.
• gpio0 o (salida 32 bits): puerto de salida de propósito general.
• gpioA i (entrada 32 bits): puerto de entrada de propósito general.
3.5.
Árbitro del bus
Esta entidad se encarga de arbitrar la utilización al bus: recibe las distintas
solicitudes de acceso al bus de los núcleos y decide cual de ellos tendrá control
sobre el mismo. Ası́ los núcleos acceden de a uno al bus, y esta entidad se encarga
de que lo hagan de manera equitativa.
3.5.1.
Descripción
A continuación se presenta el algoritmo que fue implementado para brindar
el acceso al bus de manera equitativa. Si el bus se encuentra liberado, entonces se
brinda acceso al primer núcleo que lo solicite. Los pedidos posteriores (mientras
el bus siga ocupado) se listan en un buffer FIFO en el orden que suceden. En
caso de que haya solicitudes simultáneas, se le da prioridad según el número
de núcleo, el 0 tiene prioridad frente al 1, el 1 frente al 2 y ası́ sucesivamente.
Cuando el núcleo que está controlando el bus lo libera, el siguiente valor del
buffer es procesado. Este valor indica cual será el siguiente núcleo en controlar
el bus. Si en el momento el que el bus es liberado, el buffer se encuentra vacı́o,
significa que ningún otro núcleo pretende controlar el bus. El bus pasa deja de
estar ocupado y pasa a un estado liberado, como se encontraba inicialmente. La
utilización de un buffer FIFO es fundamental para brindar control del bus a los
núcleos en forma equitativa La simulación del funcionamiento de esta entidad
se ve en la figura 3.6.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
52
Figura 3.6: Simulación del árbitro del bus, donde se observa que el la utilización del bus se reparte en forma equitativa entre los núcleos y el acceso al
mismo se va cediendo en el orden en el que llegan las solicitudes.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
busBusy_o
clk_i
reset_i
53
Árbitro
del bus
busRequest_i
BusAccess
Enable_o
busValid_o
Figura 3.7: Puertos de entrada y salida de la entidad del árbitro del bus.
El buffer utilizado debe tener al menos la misma cantidad de posiciones que
el número de núcleos del procesador, para contemplar el peor caso, que es cuando todos los núcleos solicitan acceso simultáneamente. El buffer se implementa
de manera circular, para ello en el código VHDL se utilizaron algunas operaciones en módulo [11]. El sintetizador sólo puede implementar operaciones en
módulo de potencias de dos. Estas operaciones al trabajar con números binarios
son fácilmente implementables, sólo es cuestión de tomar un cierto rango de
bits. Por ejemplo, si se tiene una señal de 32bits, aplicar operaciones en módulo
dos, implica quedarse con el último bit, en módulo cuatro con los últimos dos
bits y ası́ sucesivamente. Por el contrario operaciones en cualquier otro módulo
se vuelven mucho más complejas de implementar. Aquı́ es donde reside la limitación del plasma multi-núcleo de poder ser sintetizado únicamente con un
número de núcleos igual a potencias de dos. Modificando un poco el código de
VHDL esta limitación puede evitarse. La forma de modificarlo es tomando un
buffer siempre de tamaño igual a una potencia de dos, y también mayor al número de núcleos. Por ejemplo si se tienen tres núcleos tomar un buffer de cuatro
posiciones, si se tienen cinco, seis o siete tomar uno de ocho, y ası́ análogamente
para cualquier otro valor. El buffer sigue cumpliendo perfectamente su función.
La contra que implica la modificación es que el buffer circular dejarı́a de tener
el número mı́nimo de posiciones (igual al número de núcleos), desperdiciando
las posiciones adicionales. Esto no representa un gran costo a nivel de hardware
y la contra se vuelve insignificante. Esta modificación queda pendiente para el
futuro, y no tiene mayor importancia para este trabajo.
Las señales busAccessEnable o, busBusy o y busValid o son redundantes,
ya que la señal es de habilitación del bus, es cero cuando busValid s es cero
y es busBusy negado cuando es uno. Cada núcleo podrı́a saber deducirla internamente, pero se deja de esta manera pensando en cambiar el bus por uno
switcheado, donde dejan de ser redundantes.
3.5.2.
Puertos de la entidad
La entradas y salida listadas a continuación son para el procesador plasma
multi-núcleo con N núcleos (ver figura 3.7):
clk i (entrada): para sincronizar.
reset i (entrada): para resetear la entidad.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
54
busRequest i (entrada N bits): una señal por cada núcleo, esta es la manera
que tiene lo núcleos de realizar la solicitud del bus.
busBusy o (salida N bits): una señal hacia cada núcleo, para indicar cuando el bus esta ocupado.
busAccessEnable o (salida N bits): una señal hacia cada núcleo, para indicar cuando el núcleo tiene el acceso.
busValid o (salida): indica cuando los datos en bus son válidos, o lo que
es lo mismo cuando uno de los núcleos tiene el control del mismo. En el
caso en que ningún núcleo tiene control sobre el bus, el mismo tiene datos
inválidos.
3.6.
Manejador de interrupciones
Como todo procesador necesita un controlador de interrupciones, este en
particular está diseñado para el plasma multi-núcleo y se encarga de identificar
cuando existe una interrupción y a la vez de repartir estas interrupciones de
manera equitativa entre los distintos núcleo.
3.6.1.
Descripción
La entidad identifica las interrupciones haciendo una operación lógica AND
entre la máscara de interrupciones y el vector de estado de interrupciones. En
caso de ser distinto de cero implica que existe una solicitud de interrupción
que no fue enmascarada y alguno de los núcleos debe atenderla. La entidad
traslada la solicitud al primer núcleo. Al identificar una nueva interrupción la
solicitud es trasladada al siguiente núcleo. La entidad reparte las interrupciones
equitativamente entre todos los núcleos.
Una desventaja de esta implementación es que el manejador de interrupciones no puede identificar si el núcleo al que traslada la solicitud tiene las
interrupciones habilitadas o no. La solicitud no podrá ser atendida hasta que el
núcleo en cuestión no habilite las interrupciones, lo cual puede provocar un aumento en la latencia del procesador para atender una interrupción. Usualmente
un procesador no deberı́a tener interrupciones deshabilitadas por un largo tiempo, por lo que no serı́a un gran problema. Por el contrario, si se pretende generar
un sistema que pueda asegurar una latencia máxima para atender las interrupciones, este manejador deberı́a ser modificado, de modo que sı́ identifique que
núcleo puede ser interrupido.
3.6.2.
Puertos de la entidad
En la figura 3.8 se muestra la entidad del manejador de interrupciones.
clk i (entrada): Para sincronizar.
reset i (entrada): Para reiniciar la unidad.
irqMask i (entrada8 bits): Recibe la el valor del registro de la máscara de
las interrupciones, ver cuadro 3.1.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
clk_i
reset_i
Manejador
de
interrupciones
55
irqMask_i (8 bits)
irqStatus_i (8 bits)
plasmaCoreIrq_o
(N bits)
Figura 3.8: Puertos del manejador de interrupciones.
Bit del registro
0
1
2
3
4
5
6
7
Fuente de interrupción
UART - dato disponible
UART - disponible para escribir
bit 18 del contador
bit 18 del contador negado
No asignado, valor constante 0
No asignado, valor constante 0
bit 31 del GPIA
bit 31 del GPIA negado
Cuadro 3.1: Mapeo de los bits del registro de estado y máscara de interrupciones. Los dos primeros son para la comunicación UART. Se utilizan el
bit 18 del contador y este mismo negado, para proporcionar interrupciones cada
10ms aproximadamente. El bit 31 del puerto de A (entradas) del procesador
brinda una fuente de interrupción externa por nivel alto (bit 6) o bajo (bit 7 ).
irqStatus i (entrada8 bits): Recibe la el valor del registro de los estados
de las interrupciones, ver cuadro 3.1.
plasmaCoreIrq o (salida 8 bits): Las señales de interrupción hacia cada
uno de los núcleos.
3.7.
Núcleo
En esta sección se da una descripción del núcleo. Es la unidad más importante del sistema. En su interior alberga varias entidades, entre ellas la memoria
cache, el soporte de hardware necesario para locks y su propia unidad de control.
La unidad de control del núcleo es la más compleja del todo el diseño. En esta
sección también se describe el algoritmo de coherencia de cache.
3.7.1.
Descripción
En la figura 3.9 se muestra un diagrama de la arquitectura y a continuación
se listan los componentes del mismo:
CPU: Es la unidad de procesamiento original del plasma. Ningún cambio
fue realizado en la misma. Esta unidad es quien impone las direcciones
de memoria que necesita leer o escribir, impone el valor del dato y la
busAccEn
busSnoop
memBusy
busBusy
busRequest
memRequest
delayedBusAccessEnable_s
busAccessEnable_s
Buffer
Tri-state
Unidad
de
control
cpuIndex
CPU
Memoria
cache de
tags
Soporte
para locks
lockRequest
Memoria
cache de
datos
BUS (dataW, dataR, address, byteWE)
clk_i
reset_i
cpuPause_s
irq_i
Puerto A
NÚCLEO
Memoria
Ram
Interna
DataR
(32bits)
DataW
(32bits)
Address
(30bits)
byteWE
(4bits)
reset_i
reset
Regisitros
clk_i
clk
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
56
Puerto B
Figura 3.9: Diagrama en bloque de la arquitectura de cada núcleo.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
57
habilitación por byte de una escritura. Es a quien se debe entregar los
datos/instrucciones leı́dos. El dato puede provenir de memoria interna,
externa, cache, y otros componentes, que se ven con detalle a continuación.
Bus transceivers: Son utilizados para acoplarse al bus de interconexión.
Cuando el núcleo controla el bus impone los valores propios de dirección,
de dato a escribir y de habilitación por byte en el bus, el dato de lectura
presente en el bus es el correspondiente a la dirección del bus. Cuando
el núcleo no tiene acceso al bus esta entidad procura presentar una alta
impedancia hacia el bus, de esta manera otro núcleo puede imponer los
valores deseados. Se muestran en la figura 3.10.
Memoria ram Interna: Es una memoria sincrónica interna exclusiva de cada núcleo. Al ser sincrónica es direccionada con las señales de addressNext s
y weNext s. Por su parte la señal dataW s siempre tiene con un ciclo de
anticipación el valor correcto del dato que desea escribir. El contador de
programa de los núcleos se inicializa en la primer posición de esta memoria, de esta manera cada núcleo puede correr un programa independiente.
Se realizaron dos implementaciones para esta memoria: una genérica y
otra utilizando las librerı́as de Xilinx llamadas Unisims. Las librerı́as proveen una forma eficiente de implementar memorias en FPGAs de Xilinx,
aprovechando espacio y tiempo de sı́ntesis.
Soporte de hardware para el ı́ndice de CPU: Es un registro constante que
almacena el ı́ndice del núcleo. El multiplexor, que se explica a continuación, direcciona este valor hacia la señal de lectura de datos del CPU
(cpuDataR s).
Multiplexor del dato de lectura del CPU (cpuDataR s): Esta unidad selecciona el dato que sera leı́do por el CPU. El dato a leer es seleccionado por la
señal proveniente de la unidad de control del núcleo, cpuDataRSelection s,
y en un caso especial por la dirección del CPU. Los posibles valores pueden
verse en el cuadro 3.2.
Memoria cache: Esta compuesta por dos memorias sincrónicas de dos puertos cada una, A y B. La primera de las memorias guarda los datos y la
segunda los indicadores (tags en inglés). La memoria de datos tiene una
ancho de palabra de 32 bits y la memoria de indicadores un ancho de
diez bits. Son nueve bits de tag y uno para indicar la validez del dato.
El puerto A se utiliza para atender solicitudes del CPU y el B para la
implementación del algoritmo de coherencia de cache, de esta manera la
implementación es más simple. Se deben considerar los casos en donde la
dirección del puerto A y B coinciden. El caso donde sea una lectura en
ambos puertos no existe ningún problema. En los casos donde haya una
escritura la única a tener en cuenta es cuando el puerto B realiza una
escritura y el A una lectura, los otros dos casos, donde A realiza una escritura, son problemas a resolver por el programador, ya que no se puede
hilar tan fino sobre un recurso compartido, o puede ser que se está accediendo a recursos compartidos sin protegerlos por locks. El único caso
que debe considerarse es cuando se lee un dato a través del puerto A y
al mismo tiempo el puerto B trata de escribir un nuevo dato. Se permite
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
58
Condición
Fuente del dato leı́do por
el CPU (cpuDataR s)
Comentario
Dirección del CPU
apunta a 0X1000
cpuDataRSelection s
= "00"
cpuDataRSelection s
= "01"
cpuDataRSelection s
= "01"
Indice del CPU
(coreNumber)
Datos de la memoria interna
(internalRamDataR s)
Datos de memoria cache
(cacheDataRA s(31 downto 0))
Datos del bus
(busDataR i)
cpuDataRSelection s
= "01"
No interesa
Soporte para la identificar del
número de CPU (reg. constante)
Para toda lectura de datos o
inst. de la memoria interna.
Cuando la lectura en
cache es un hit.
Al leer memoria externa
(cacheable o no cacheable),
cuando se produce un
miss en una lectura
o se leen componentes externos.
Los datos no son leı́dos por el
CPU, por lotanto no interesa.
Cuadro 3.2: Posibles fuentes para la señal cpuDataR s
.
la escritura del dato a través del puerto B y el puerto A en vez de leer el
dato almacenado en cache entrega el dato de escritura del puerto B.
Cuando el CPU realiza una escritura que no es de 32 bits a través del
puerto A se invalida esa posición de memoria, para evitar malas lecturas
en el resto de la palabra. Por ejemplo: si un dato se encuentra almacenado
en cache y se realiza la escritura de uno sólo de sus bytes, todo funciona
correctamente. Lo mismo sucede con un cache update, ya que el dato es
escrito en las caches de otros núcleos sólo si si el dato ya se encontraba
albergado en las mismas. El problema aparece al realizar una escritura de
un dato que no se encuentra en cache. Sólo es posible a través del puerto
A, ya que el puerto B sólo realiza escrituras de datos que sı́ se encuentran
en cache (actualiza datos, nunca reemplaza los bloques que ya están en
cache). Si se escribe el byte cero de una palabra que no está cacheada,
la polı́tica de escritura en cache write-allocate, obliga a escribir el dato
en cache, y sólo se escribe el byte cero y el resto queda con valores no
necesariamente válidos. Al realizar una lectura de cualquier otro de los
bytes de esa palabra, el dato leı́do es entonces erróneo. Tampoco es posible
simplemente no escribir el dato, ya que si dato sı́ se encuentra en memoria
cache, al realizar la escritura de un byte de una palabra, el dato tampoco
es actualizado.
Dicho esto se plantean tres soluciones:
1. Si se realiza una escritura que no sea de 32 bits en un dato que
no se encuentre albergado en memoria cache, primero se busca el
dato en memoria y se lo copia en cache Luego se escribe el byte
correspondiente (en cache y memoria).
2. Invalidar el dato en una escritura que no sea de 32 bits: El bit más
significativo del tag es en realidad el bit de dato válido. Al colocar
éste último bit en un estado lógico uno, el dato se invalida.
3. Al realizar una escritura que no sea de 32 bits se comprueba si el
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
59
dato se encuentra en cache o no. En caso de que sı́ se encuentre se
escribe, y en caso de que no se encuentre no se realiza nada. Serı́a
la solución más efectiva, ya que no invalida datos innecesariamente y
actualiza los bytes de un dato cacheado si es necesario. El problema
que tiene esta solución es que es necesario acceder dos veces a cache,
primero para leer el indicador y compararlo con el del dato a escribir,
y luego, en caso de detectar que el dato sı́ se encuentra en cache,
realizar la escritura del byte correspondiente. Al ser la memoria de
lectura sincrónica es posible realizarlo en un sólo ciclo de reloj. Si
fuera asincrónica el tag puede ser leı́do, luego comparado con el otro
tag y finalmente el resultado de la comparación puede ser utilizado
para habilitar la escritura en memoria cache, todo en el mismo ciclo
de reloj. Al utilizar una memoria sincrónica son entonces necesarios
al menos dos ciclos de reloj y su implementación se vuelve algo más
complicada.
La opción (1) se descarta por ser totalmente ineficiente, y la (3) también es descartada, porque al utilizar memoria sincrónica se vuelve más
complicada que la (2), que es la implementación adoptada.
Registros sincrónicos: Son un conjunto de registros sincrónicos que retrasan algunas señales un ciclo de reloj, para poder conservar sus valores. Las
señales que se almacenan en estos registros y la necesidad de los mismos
se detalla en la sección 3.7.3.
Soporte de hardware para los locks: En la dirección de memoria 0x200000A0
se mapea un registro virtual, que al escribirlo con cualquier valor distinto
de cero, provoca que la señal lockRequest o tome un valor lógico uno, y
toma un estado lógico cero al ser escrito con un valor nulo. Su implementación se muestra a continuación:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
l o c k R e q u e s t : process ( c l k i , r e s e t i )
begin
i f r e s e t i = ’ 1 ’ then
l o c k R e q u e s t o <= ’ 0 ’ ;
e l s i f r i s i n g edge ( c l k i ) then
i f cpuAddress s & ” 00 ” = X” 200000A0” and cpuWe s /
= ” 0000 ” then
i f cpuDataW s = X” 00000000 ”then
l o c k R e q u e s t o <= ’ 0 ’ ;
else
l o c k R e q u e s t o <= ’ 1 ’ ;
end i f ;
end i f ;
end i f ;
end process ;
La señal lockRequest o se envı́a al manejador de locks del plasma multinúcleo, quien luego decide que núcleo obtiene el lock.
Multiplexor del puerto A de la cache(dirección, habilitación por byte, dataW): mediante de este multiplexor se seleccióna qué señales controlan al
puerto A de la cache. Las señales que se multiplexan son la dirección de
lectura/escritura, el dato a escribir, y la habilitación por byte. Hay dos
posibles fuentes para estas señales:
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
60
• Al realizar un write-through o una lectura de cache, los datos provienen del CPU:
◦ Dirección de cache: cpuAddresNext s, es la única señal que nos
interesa en una lectura.
◦ Habilitación por byte: cpuWeNext s.
◦ Dato a escribir en cache: cpuDataW s, el dato a escribir por el
CPU se encuentra disponible con un ciclo de anticipación, por
eso se puede utilizar la señal cpuAddresNext s y cpuWeNext s.
◦ Tag: Son los bits 19 a 11 de la dirección de escritura, es decir
cpuAddressNext s(19 downto 0).
• Al producirse un miss de lectura, los datos provienen del bus:
◦ Dirección de cache: busAddres io.
◦ Habilitación por byte: busWe io.
◦ Dato a escribir en cache: busDataW io.
◦ Indicador: Son los bits 19 a 11 de la dirección de escritura, es
decir busAddress s(19 downto 0).
Unidad de control: es la unidad de control del núcleo y sin duda la parte
más importante del mismo. Se detalla su funcionamiento en la sección 3.8.
3.7.2.
Puertos de la entidad
En la figura 3.10 se muestran los puertos listados a continuación junto con
los tranceivers del bus:
clk i (entrada): para sincronizar.
reset i (entrada): para reiniciar la unidad.
irq i (entrada): señal para generar una excepción en el núcleo.
Señales de solicitud de recursos compartidos:
• lockRequest o (salida): señal que utiliza el núcleo para solicitar el
lock, descripta en la sección 3.4. Esta señal es recibida por la entidad que administra el lock entre todos los núcleos. Un estado lógico
‘uno’es una solicitud y ‘cero’es una no solicitud o liberación del lock.
• busRequest o (salida): señal de solicitud de uso del bus, se envı́a a la
entidad árbitro del bus descripta en la sección 3.5.
• busAccessEnable i (entrada): señal recibida desde el árbitro del bus
que indica cuando se tiene acceso al bus.
• busBusy i (entrada): señal recibida desde el árbitro del bus que indica
cuando el bus se encuentra ocupado.
• memRequest o (salida): señal de solicitud de acceso a memoria principal. El plasma multi-núcleo toma las solicitudes de memoria de
todos los núcleos y genera una única señal de solicitud que es enviada al controlador de memoria. De todos los núcleos que soliciten
la memoria el que tiene acceso es el mismo que tiene acceso al bus.
Esta señal funciona en conjunto con la de solicitud del bus, o bien el
núcleo solicita acceso al bus (Por ejemplo al acceder a los periféricos)
o solicita acceso al bus y a memoria simultáneamente.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
61
clk_i
reset_i
busAddres_io
irq_i
busDataW_io
memBusy_i
BUS
busAccessEnable_i
Núcleo
busSnoop_i
busWE_io
busBusy_i
busRequest_o
busDataR_i
lockRequest_o
memRequest_o
Figura 3.10: Puertos de entrada y salida de cada uno de los núcleos del plasma
multi-núcleo. Se muestra también como tres de las salida tiene buffers tri-state,
necesarios para conectarse al bus. A su vez se tienen entradas directas desde el
bus, éstas son necesarias para recibir mensajes de broadcast, necesarios para la
implementación del algoritmo de coherencia de cache elegido.
• memBusy i (entrada): indica cuando la memoria se encuentra ocupada. Esta señal puede ser activada por varias razones, por ejemplo
durante la inicialización de la memoria, o cuando algún núcleo realiza
un acceso a memoria principal como se explicó en la sección 3.3, o
podrı́a ser en el caso de que se implemente algún dispositivo DMA2 .
Señales del bus de datos/instrucciones y de coherencia de cache:
• busSnoop i (entrada): Señal para la implementación de la coherencia
de cache, que indica que los datos del bus deben ser leı́dos para luego
verificar si se debe actualizar la cache o no.
• busAddress io (entrada/salida 30 bits): Señal para acceder a memoria
y para la coherencia de cache. Es la dirección que se impone en el
bus.
• busDataW io (entrada/salida 32 bits): Señal para acceder a memoria
y para la coherencia de cache. Cuando es salida es el dato que escribe
en memoria. Cuando es entrada es el dato escrito por otro núcleo en
memoria.
2 En inglés Direct Memory Access, es la manera que tienen entidades que no son núcleos de
procesamiento de acceder a la memoria directamente sin necesidad de hacerlo a través de un
CPU, de esa manera no le quitan tiempo de procesamiento. Estas entidad realizan únicamente
movimiento de datos sin procesamiento alguno.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
62
• busWe io (entrada/salida 4 bits): Señal para acceder a memoria y
para la coherencia de cache. Es la habilitación por byte del dato a
escribir puede ser propia o de otro núcleo.
• busDataR i (entrada 32 bits): Sólo para el acceso a memoria en particular cuando se realiza una lectura.
3.7.3.
Descripción del algoritmo de coherencia de cache
El plasma multi-núcleo brinda cierto soporte al algoritmo, como ser las
señales de snooping. Pero es núcleo quien alberga al resto del hardware necesario
para su implementación. La unidad de control del núcleo es quien lógicamente
controla el algoritmo.
Se utiliza un protocolo de snooping (sección 2.4.5). El algoritmo de coherencia es muy simple, y se detalla a continuación. Hay una serie de elementos
importantes que dan lugar a este algoritmo tan sencillo:
1. La interconexión de los núcleos se realiza a través de un bus simple.
Los núcleos se conectan al bus a través de buffers tri-state. Las señales
cpuDataW s, cpuAddress s y cpuWe s pueden ser impuestas en el bus si el
núcleo posee el acceso. Y cuando no tiene el acceso el núcleo puede leer los
datos impuestos por el núcleo que sı́ controla al bus. Esto último sumado
a la señal busSnooping permiten el envı́o de mensajes de broadcast entre
los núcleos.
2. La señal busSnooping indica cuando hay un mensaje de broadcast en el
bus al ponerse en un estado lógico uno.
3. La polı́tica de escritura en memoria write-throug obliga a escribir los datos directamente en memoria principal. En una polı́tica algo ineficiente,
pero de ésta manera se logra que la memoria principal siempre tenga los
datos válidos y más importante aún, obliga a que la información sobre la
modificación de algún dato viaje siempre por el bus.
Éstos items permiten que al modificar cualquier dato en memoria se envı́e a
la vez un mensaje de broadcast a todos los núcleos, con la información del nuevo
dato. Cada núcleo lee este mensaje y en caso de tener almacenado el dato en
cache lo actualiza con el nuevo dato. A un nivel más bajo, esto implica la lectura
del tag en memoria cache cada vez que se recibe la señal busSnooping . Como
la memoria es sincrónica, el resultado de la lectura se obtiene un ciclo después.
Nada asegura que un ciclo después el dato siga estando en el bus para realizar la comparación3 , por lo que los datos del bus necesarios son guardados en
registros. Las señales necesarias son busAddress io, busWe io, busDataW io y
busAccessEnable i, son almacenadas en delayedBusAddress s, delayedBusWe s,
delayedBusDataW s y delayedBusAccessEnable s respectivamente. Una vez
se obtiene el indicador de la cache se compara con el indicador del registro
(delayedBusAddress io(19 downto 11). Si los tags coinciden y el núcleo no
3 En esta implementación se utiliza la memoria del kit Nexys2 y el acceso a memoria tarda
más de un ciclo. El dato se mantendrı́a en el bus por más de un ciclo y no serı́an necesarios
los registros, pero si la memoria a la que se accede es interna (sea la memoria principal o una
cache de nivel dos o inclusive un buffer de escritura), la lectura puede durar un ciclo y los
registros se vuelven necesarios.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
63
fue el que realizó la escritura, entonces la señal cacheUpdate s toma un valor
lógico ‘uno’, sino ‘cero’. No tiene sentido actualizar el dato en cache del núcleo
que realiza la escritura, ya que el dato que se encuentra en cache es obligatoriamente el correcto por la polı́tica de write-allocate. La señal delayedBusAccessEnable s
indica si el núcleo realizó o no la escritura del dato, si en el ciclo anterior tuvo
acceso al bus, es quien realizó la escritura.
Mediante la señal cacheUpdate s se le indica al puerto B que debe escribir
el dato guardado en los registros. El indicador no es escrito a través del puerto
B, ya que si se actualiza la cache, es porque el indicador de cache igual al de los
registros. El banco de indicadores sigue estando disponible para la lectura en
caso de que se produzcan escrituras en ciclos de clock consecutivos, en este caso
no es posible eso por lo anteriormente mencionado, que la escritura se realiza
en una memoria lenta, y no se realiza en más de un ciclo de reloj. En la figura
3.11 se muestra una simulación del proceso de actualización de cache. En la
simulación los cuatro núcleos realizan una operación de escritura de una misma
posición de memoria.
3.8.
3.8.1.
Unidad de control del núcleo
Descripción
La unidad controlador del núcleo es una máquina de estados con ocho estados (ver figura 3.12). Los estados dependen del tipo de memoria a la que se
esta accediendo, sea interna, externa o cache. Todos los periféricos y registros
especiales del núcleo están mapeados en memoria, por lo que también son considerados como una memoria más. El estado también depende del tipo de acceso,
o sea si es una escritura o o una lectura. En total se tienen ocho estados:
0. INTERNAL RAM: lectura/escritura en memoria interna.
1. CHECKING: Lectura en cache. Durante este estado también se comprueba
el indicador leı́do, y en caso de coincidir se considera la lectura un hit, de
lo contrario un miss.
2. WT0 (write-through): escritura en memoria externa, también se escribe
en memoria cache, debido a la polı́tica de escritura write-allocate.
3. WT1 (write-through): estado necesario si se pretende volver a realizar una
lectura en cache después de un WT0, se explica más adelante.
4. MISC COMPONENTS: lectura/Escritura de registros especiales, en el
cuadro 3.3 se muestran los mapeos en memoria.
5. NON CACHEABLE: lectura/Escritura de la porción de memoria que no
puede ser cacheada.
6. MISS0: luego de un CHECKING, si se produce un miss el dato debe ser
leı́do de memoria externa. Ésto es lo que indica este estado.
7. MISS1: Si luego de un miss se pretende leer cache nuevamente, se debe
pasar por este estado, según se explica a continuación.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
64
Figura 3.11: Simulación del proceso de actualización de cache. En la
figura se ve una simulación de un procesador con cuatro núcleos. En esta sección
del código ejecutan los cuatro una instrucción de escritura en la misma posición
de memoria. Los momentos en los que cada uno de los núcleos inicia la escritura
de memoria se pueden ver en la señal de busSnoop s. La primer escritura no
genera ninguna actualización, señal cacheUpdate s. En la segunda se actualiza
el dato recientemente cacheado en el núcleo uno, en la tercera se actualizan los
datos en los núcleos uno y dos, y por último cuando el núcleo cuatro es quien
escribe en memoria los núcleos uno, dos y tres actualizan la cache.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
65
Figura 3.12: Estados y transiciones de la máquina de estados de la unidad de
control.
Al existir un miss o un write-thorugh se debe escribir un dato en la memoria
cache. La escritura de este dato se hace durante los estados WT0 y MISS0. Si
luego de estar en alguno de estos estados se pretende ir al estado CHECKING,
se produce un conflicto, la señal de dirección de memoria cache del puerto A
debe tomar dos valores al mismo tiempo, la dirección de escritura del dato y
la dirección de lectura de la siguiente instrucción. Para solucionar este conflicto
se introducen los estados WT1 y MISS1. Durante WT0 y MISS0 la señal de
dirección del puerto A tiene la dirección correspondiente al dato a escribir en en
memoria cache y en los estados WT1 y MISS1 la señal de dirección del puerto A
tiene la dirección del dato/instrucción a leer, para chequear el tag en el siguiente
ciclo, mientras se está en el estado CHECKING.
Dentro de la unidad de control hay dos señales llamadas state s y stateNext s,
que tienen información del estado actual y del estado siguiente respectivamente.
state s es una señal sincrónica, mientras que stateNext s se obtiene con lógica
combinacional a partir de las entradas y del estado actual.
Las salidas de esta entidad son las señales de control de todo el procesador.
Las salida pueden depender de state s y de stateNext s. En este caso al depender del estado siguiente, que a su vez depende de las entradas, esta máquina
es una máquina de Mealey. Durante el diseño de esta entidad se tuvo que prestar
especial atención a las dependencias entre las entradas y las salidas, para evitar
generar lazos combinacionales.
3.8.2.
Puertos de la entidad
En la figura 3.13 se muestra esta entidad.
clk i (entrada): Para sincronizar.
reset i (entrada): Para reiniciar la unidad.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
Dirección
de Memoria
0x00000000
...
0x00000FFC
0x00001004
66
Mapeo
Memoria interna
Índice del CPU
0x00001004
...
0x0FFFFFFC
0x10000000
...
0x100FFFFF
0x10100000
...
0x103FFFFF
0x10400000
...
0x1FFFFFFF
0x20000000
0x20000010
0x20000020
0x20000030
0x20000040
0x20000050
0x20000060
0x20000080
0x200000A0
0x20000000
...
0xFFFFFFFC
No asignada
Memoria
externa
cacheable
Memoria
externa
no-cacheable
No asignada
Lectura/escritura UART
Máscara de interrupciones
Estado de interrupciones
GPI0 set-bits/lectura
GPI0 clear-bits/lectura
GPIA lectura
Contador
Registro de comunicación
Registro del lock
No asignada, salvo
las direcciones
antes nombradas
Cuadro 3.3: Mapeo de la memoria. Las direcciones apuntan a cada byte de
la memoria. Como utilizamos palabras y registros de 32 bits, las direcciones van
de cuatro en cuatro. Ası́ se define en la ISA de MIPS.
cpuAddress i (entrada30 bits): Proveniente del mliteCpu.
cpuWe i (entrada4 bits): Proveniente del mliteCpu.
cpuAddressNext i (entrada30 bits): Proveniente del mliteCpu.
cpuWeNext i (entrada4 bits): Proveniente del mliteCpu.
busSnoop i (entrada): Señal necesaria para implementar el algoritmo de
coherencia de cache. Indica cuando hay un mensaje de broadcast en el bus.
busAccessEnable i (entrada): Provenientes del árbitro del bus. Indica cuando se tiene acceso al bus.
busBusy i (entrada): Provenientes del árbitro del bus. Indica cuando el
bus esta ocupado.
memBusy i (entrada): Provenientes del controlador de memoria externa.
Indica cuando la memoria esta ocupada.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
67
clk_i
reset_i
cacheUpdate_o
cpuAddres_i
cpuWe_i
cacheAddressASource_o
cacheWEASource_o
cacheDataRASource_o
cacheEnableA_o
cpuAddresNext_i
CpuWeNext_i
busSnoop_i
busAccessEnable_i
busBusy_i
memBusy_i
cacheTagA_i
cacheTagB_i
delayedBusAddress_i
delayedBusAccessEnable_i
delayedBusWe_i
Unidad
de
control
cacheEnableB_o
internalRamEnable_o
cpuPause_o
cpuDataRSource_o
busRequest_o
memRequest_o
busTransceiversEnable_o
Figura 3.13: Unidad de control que posee cada uno de los núcleos de plasma
multi-núcleo
cacheRamTagA i (entrada10 bits): Proveniente de la cache, es el indicador
leı́do de la cache a través del puerto A, incluye el bit de validez.
cacheRamTagB i (entrada10 bits): Proveniente de la cache, es el indicador
leı́do de la cache a través del puerto B, incluye el bit de validez.
delayedBusAddress i (entrada30 bits): Proveniente de los registros del
núcleo. Brinda la dirección presente en el bus con un ciclo de retraso.
delayedBusWe i (entrada(30 bits): Proveniente de los registros del núcleo.
Brinda la habilitación de escritura por byte presente en el bus con un ciclo
de retraso.
delayedBusAccessEnable i (entrada): Proveniente de los registros del núcleo.
Brinda información del acceso al bus de éste núcleo con un ciclo de retraso.
cacheUpdate o (salida): Señal que indica que se debe actualizar la cache
con los valores de los registros.
cacheRamAddressASource o (salida2 bits): Se envı́a al multiplexor del
puerto A, selecciona la dirección del puerto.
cacheRamWeASource o (salida3 bits): Se envı́a al multiplexor del puerto
A, selecciona la habilitación por byte del puerto.
cacheRamDataWASource o (salida2 bits): Se envı́a al multiplexor del puerto A, selecciona el dato a escribir del puerto.
cacheRamEnableA o (salida): Es la habilitación del puerto A de la cache.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
68
address
clk
byte_we
reset_in
data_r
intr_in
mem_pause
mliteCpu
data_w
address_next
byte_we_next
Figura 3.14: Puertos de entrada y salida de CPU del plasma original.
cacheRamEnableB o (salida): Es la habilitación del puerto B de la cache.
cpuPause o (salida): Es la señal de pausa para el mliteCpu.
cpuDataRSelection o (salida): Se envı́a al multiplexor del mliteCpu. Selecciona la fuente del dato de lectura del CPU.
internalRamEnable o (salida): Es la habilitación de la memoria interna
del CPU.
busRequest o (salida): Se envı́a al árbitro del bus. El la solicitud para la
utilización del bus.
busTransceiverEnable o (salida): Se envı́a a los buffers tri-state, es la habilitación para que impongan la señal en el bus.
memRequest o (salida): Se envı́a al controlador de memoria. Es la solicitud
para acceso a memoria.
3.9.
CPU plasma
Como se dijo anteriormente el CPU no se rediseñó desde cero, sino que se lo
utiliza tal cual se extrajo del plasma original. En la figura 3.14 se muestra la interfaz de conexión del CPU. No se analiza su estructura interna, sólo su interfaz.
Todas las salida de este bloque son secuenciales, salvo las salidas address next
y byte we next, cuyo valor dependen del valor del la entrada mem pause, puede
verse en la figura 3.15. Es importante tener esto en cuenta, ya que el valor de la
entrada mem pause no puede depender directamente ni del valor de address next
ni byte we next, ya que se generarı́a un lazo combinacional.
3.10.
Caracterización del procesador
En esta sección se evalúan caracterı́sticas de tamaño y desempeño del procesador multi-núcleo. Se muestran los resultados de las implementaciones y mediciones realizadas y se hace un análisis de los mismos. El procesador multi-núcleo
muestra mejoras en el rendimiento al ejecutar los programas de prueba, respecto
a la implementación mono-núcleo del plasma.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
69
Figura 3.15: Interfaz del plasma original. Al analizar su interfaz se encontró que las salidas address next y byte we next dependen convinacionalmente
del valor de la entrada mem pause.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
Número de núcleos
1
2
4
70
Slices Utilizadas
2635
4624
8030
Cuadro 3.4: Tamaño del procesador plasma
3.10.1.
Tamaño del procesador
En el cuadro 3.4 se puede ver el tamaño en slices que ocupa el plasma multinúcleo. En estas condiciones se pueden implementar hasta cuatro núcleos en
una FPGA Spartan3E-1200. Las evaluaciones de rendimiento que se muestran
en este trabajo se realizan en procesadores de uno a cuatro núcleos.
3.10.2.
Desempeño en función del trabajo de las tareas
Los siguientes resultados surgen de ejecutar distintos programas de pruebas
en el procesador, en cada programa de prueba se aumentaba la cantidad de
instrucciones. La figura 3.16 muestra como mejora el rendimiento en el procesamiento a medida que aumenta la cantidad de trabajo que realizan las tareas
de prueba, el trabajo que realizan las tareas se mide en número de instrucciones
que ejecutan. Los resultados mostrados son para tres tareas distintas, donde la
fracción de instrucciones que acceden a memoria cambia. En estas gráficas en
particular se ven tareas con un 15, 00 %, 4, 17 % y 3, 22 % de tareas que acceden
al bus del procesador. Las figuras 3.16(a) y 3.16(b) muestran los resultados al
comparar el procesador de cuatro núcleos frente al de uno y el de dos núcleos
frente al de uno respectivamente. Se ve que en todos los casos el rendimiento
aumenta a medida que aumenta el trabajo que realizan las tareas en sı́. Esto se
debe a que el porcentaje de tiempo que le toma al sistema operativo reescalonar
las tareas es cada vez menor. A partir de cierto valor, este tiempo se vuelve
despreciable frente al tiempo de procesamiento efectivo. También se aprecia que
siempre se llega a un lı́mite en la mejora, el cual depende del porcentaje de
instrucciones que acceden al bus. En el mejor caso de la figura 3.16(a) este valor
se acerca a 4, mientras que en la 3.16(b) a 2, que son los respectivos valores
teórico máximos que se pueden alcanzar en casos ideales.
El caso ideal es cuando no existen colisiones en el bus, y la única manera
de asegurar que las colisiones sean nulas es que ningún núcleo acceda al bus en
ningún momento, lo cual es imposible. El aumento en las colisiones provoca que
el rendimiento disminuya. Las colisiones en el bus generan un lı́mite en el rendimiento de procesadores multi-núcleo. A continuación se evalúa el rendimiento
de un procesador en función de los accesos a memorias de los núcleo.
3.10.3.
Desempeño en función de la utilización del bus
Los siguientes resultados surgen de realizar pruebas en el procesador, ejecutando programas en los cuales se aumenta la utilización del bus progresivamente.
Las colisiones en el bus aumentan al aumentar el número de núcleos y al aumentar el porcentaje de utilización del bus de cada uno de ellos. Se evalúa como
mejora la eficiencia del procesadores al disminuir las colisiones. En la figura 3.17,
se muestra que la mejora en el tiempo de procesamiento de las tareas disminuye
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
71
(a) Al pasar de uno a cuatro núcleos.
(b) Al pasar de uno a dos núcleos.
Figura 3.16: Mejora en el tiempo de ejecución al pasar de uno a cuatro núcleos.
Muestra la relación en los tiempos de ejecución en función de la cantidad de
procesamiento de las tareas que se corren. La cantidad de procesamiento se
mide instrucciones que ejecutan las tareas.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
72
Figura 3.17: Mejora en el tiempo de ejecución para cuatro y dos núcleos, frente
a un núcleo, en función del porcentaje de instrucciones que acceden al bus en
las tareas.
junto con el aumento del porcentaje de instrucciones que acceden al bus. También se ve un resultado indeseado, que es que para porcentajes muy bajos, la
mejora vuelve a disminuir. Esto se debe a que para estos valores tan pequeños,
el tiempo de procesamiento efectivo de las tareas disminuye y comienza a influir
el tiempo necesario para reescalonar las tareas, es decir deja de ser despreciable.
Para poder despreciarlo se deben elegir tareas con mayor cantidad de procesamiento, como se mostró en los resultados expuestos en las figuras 3.16(a) y
3.16(b).
3.10.4.
Tiempo de procesamiento en función del número
de núcleos
La figura 3.18 muestra resultados del tiempo de procesamiento para procesadores de uno a cuatro núcleos para tareas con distinta utilización del bus.
Nuevamente se observa como el porcentaje de instrucciones que acceden al bus
es crı́tico. Dependiendo de la frecuencia de utilización del bus la mejora es más
o menos marcada al aumentar el número de núcleos. A medida que disminuye
la utilización los tiempos mejoran en mayor medida al aumentar los núcleos de
procesamiento. En algunos casos solo se ve que existe una mejora significativa al pasar de uno a dos núcleos y no de dos a cuatro. Esto se debe al lı́mite
impuesto por las colisiones en el bus, al aumentar la cantidad de núcleos, las
colisiones aumentan. En esta gráfica también tenemos un resultado indeseado
para los casos de 2 % y 4 %, también debido al hecho de que el tiempo efectivo
de procesamiento disminuye y empieza a influir el tiempo de replanificación de
las tareas. Este efecto no se notarı́a si se ejecutasen tareas que realicen mayor
trabajo, como se mostró en los resultados de la sección 3.10.2
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
73
Figura 3.18: Tiempo de ejecución para distintas tareas en función del número de
núcleos de procesamiento. Las tareas difieren en el porcentaje de instrucciones
que acceden al bus.
3.10.5.
Importancia de la memoria cache en procesadores
multi-núcleo
Se quiere evaluar el rendimiento al correr un programa en el procesador plasma multi-núcleo sin las correspondientes memorias cache, para ellos se corren
programas de prueba en implementaciones sin la memoria cache. El resultado
es la nula mejora en el rendimiento, independientemente del número de núcleos
y del tipo de tarea, ya sea que tenga o no gran porcentaje de instrucciones de
acceso al bus. Este resultado es esperado, ya que todos los núcleos acceden al
bus por el solo hecho de tener que leer la instrucción que ejecutan. El arbitro
del bus permite el acceso al mismo de a un núcleo a la vez pausando al resto.
Tener más de un núcleo carece de sentido, ya que el procesamiento efectivo sin
memoria cache es igual o inclusive peor.
CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS
74
Capı́tulo 4
Conclusiones y trabajos
futuros
4.1.
Conclusiones
Se diseñó e implementó un sistema multi-núcleo sobre el cual se realizaron
testeos de eficiencia. Los resultados presentados muestran la dependencia del
rendimiento con el número de núcleos ,el tipo de tarea que se tenga y la memoria
cache. Los factores más determinantes para la obtención o no de una mayor
eficiencia al procesar distintas tareas son la utilización del bus y la carga de
procesamiento que éstas tengan. Se mostró también que existe un lı́mite en la
posible mejora al rendimiento y que puede estar muy alejado del caso teórico
ideal.
Existen otros factores a evaluar en un futuro, entre ellos como se ve modificado el lı́mite de una posible mejora al implementar otro tipo de memorias
cache con otras polı́ticas y que no sea compartida entre instrucciones y datos.
Otros trabajos futuros pueden ser la implementación del plasma multi-núcleo,
que soporte una cantidad de núcleos que no se necesariamente potencia de dos.
También la implementación del monitor del sistema operativo, para controlar
la correcta distribución de las tareas que se ejecutan, en ambientes de gran exposición electromagnética, que fue uno de los factores que impulsó este trabajo.
Este trabajo presenta una primer implementación del plasma multi-núcleo.
Hay una gran cantidad de posibilidades para seguir mejorando el diseño.
4.2.
Trabajos Futuros
En primer lugar listaremos trabajos futuros que requieren pocas modificaciones a la arquitectura y serı́an casi inmediatas:
Modificar el código en las secciones que sean necesarias para poder implementar un número de núcleo que no deba ser necesariamente potencia de
dos.
75
CAPÍTULO 4. CONCLUSIONES Y TRABAJOS FUTUROS
76
Cambiar las polı́ticas de escritura de memoria cache, de write-allocate a
write-no-allocate, y luego cambiar levemente el algoritmo de coherencia
de cache, de modo que el mismo núcleo observe sus propios mensajes de
broadcast y actualice la los datos almacenados en memoria cache utilizando
el puerto B de la misma, como hacen el resto de los núcleo. Se lograrı́a
una leve mejora al no invalidar datos de memoria cache durante escrituras
que no sean de 32 bits. Una vez hecho el cambio realizar mediciones y
compara el rendimiento frente a la versión actual.
Modificar la memoria cache y hacerla únicamente de instrucciones. De esta
manera los algoritmos de coherencia de cache pueden ser simplificados,
reduciendo ası́ el hardware necesario. Comparar la diferencia de tamaño
del procesador y su eficiencia con los de la versión anterior.
Modificación de la memoria interna de cada núcleo. La cantidad de núcleos
que pueden ser implementados en en las FPGAs, están limitados hoy en dı́a
por los bloques de memoria ram diponibles en las mismas. Reduciendo la
memoria interna posibilitarı́a incrementar el número de núcleos. Realizar
nuevas mediciones.
Siguiendo la lı́nea de trabajo que se mencionó durante este trabajo, la implementación de un task scheduler monitor para el plasma multi-núcleo.
No precisa ninguna modificación al plasma. Luego realizar las experimentaciones en ambientes de alta interferencia electromagnética y/o bajo radiación nuclear.
Otro tipo de desarrollos posibles basados en estre trabajo pueden ser:
Implementación de un bus switcheado, incluyendo más de un controlador
de memoria.
Modificación de la estructura de la cache para que el tamaño de bloque
sea parametrizable.
Modificación de la estructura de la cache para que sea parametrizable la
asociatividad.
Implementación de polı́tica de escritura en memoria del tipo Write Back.
Separación de memoria caché en instrucciones y datos.
Implementación de la MMU (Memory Management Unit).
Implementación de la FPU (Floating Point Unit).
Mejoras y vectorización del manejo de interrupciones y excepciones (NVIC).
Hoy en dı́a hay al menos otros cuatro estudiantes que siguen la lı́nea de
trabajo del plama múlti-núcleo en sus trabajos de tesis de grado. Ellos tratarán
algunos de los temas anteriormente nombrados.
Bibliografı́a
[1] Michael J. Flynn, Computer Architecture. Jones & Bartlett Learning, 1995.
[2] John L. Hennessy, David A. Patterson Computer Architecture - A Quantitative Approach, 4th Edition. Morgan Kaufman Publishers, 2007.
[3] David A. Patterson, John L. Hennessy Computer Organization and Design
- The Hardware/Software Interface, 3rd Edition. Morgan Kaufman Publishers, 2005.
[4] Procesador Plasma y PlasmaOS: http://opencores.org/project,plasma
[5] Jan M. Rabaey, Anantha Chandrakasan, and Borivoje Nikolic Digital Integrated Circuits, A Design Perspective, 2nd Edition, Prentice-Hall
[6] Arquitectura MIPS: http://www.mips.com/
[7] David E. Culler, Jaswinder Pal Singh, Parallel Computer Architecture: A
Hardware/Software Approach 1998.
[8] J. Tarrillo, L. Bolzani, F. Vargas, A Hardware-Scheduler for Fault Detection
in RTOS-Based Embedded Systems 2009.
[9] D. Silva, K. Stangherlin, L. Bolzani, F. Vargas, A Hardware-Based Approach to Improve the Reliability of RTOS-Based Embedded Systems 2011.
[10] Andrew S. Tanenbaum, Modern Operating Systems, 2nd edition. Prentice
Hall, 2002.
[11] Aritmética modular, http://en.wikipedia.org/wiki/Modular arithmetic, Wikipedia.
[12] Benchmarking, http://es.wikipedia.org/wiki/Benchmark, Wikipedia.
77
BIBLIOGRAFÍA
78
Apéndice A
Implementaciones de
spinLocks
A.1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Con soporte de hardware
u i n t 3 2 OS SpinLock ( void )
{
uint32 state ;
u i n t 3 2 cpuIndex = OS CpuIndex ( ) ;
s t a t e = OS A sm I nt er r up t En ab l e ( 0 ) ; // d i s a b l e i n t e r r u p t s
i f ( MemoryRead ( 0 x200000A0 ) == (1<<cpuIndex ) )
return ( u i n t 3 2 ) −1;
MemoryWrite ( 0 x200000A0 , 0 x f f f f f f f f ) ; // l o c k R e q u e s t
f o r ( ; ; ) // w a i t f o r l o c k
{
i f ( MemoryRead ( 0 x200000A0 ) == (1<<cpuIndex ) )
break ;
}
a s s e r t ((1<<OS CpuIndex ( ) )==MemoryRead ( 0 x200000A0 ) )
return s t a t e ;
}
void OS SpinUnlock ( u i n t 3 2 s t a t e )
{
a s s e r t ((1<<OS CpuIndex ( ) )==MemoryRead ( 0 x200000A0 ) )
i f ( s t a t e == ( u i n t 3 2 ) −1)
return ;
// n e s t e d l o c k c a l l
MemoryWrite ( 0 x200000A0 , 0 x0 ) ; // c l e a r l o c k R e q u e s t
OS A sm I nt e rr up t En ab l e ( s t a t e ) ; // r e s t o r e i n t e r r u p t s
}
79
APÉNDICE A. IMPLEMENTACIONES DE SPINLOCKS
A.2.
80
Solución propuesta por Peterson
Es una solución por software mostrada en [10], propuesta por G. L. Peterson
en el año 1981. La solución es más simple que las que existente en esa época. El
código presentado a continuación es identico al del libro, a pesar de que parezca
genérico sólo funciona para dos núcleos.
Cada proceso debı́a llamar a la función entre region, antes de utilizar un
recurso compartido, y a la función leave region al terminar la utilización del
mismo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#DEFINE FALSE 0
#DEFINE TRUE 1
#DEFINE N
2
/∗ number o f p r o c e s s e s ∗/
int turn ;
i n t i n t e r e s t e d [N ] ;
/∗ whose t u r n i s t i ? ∗/
/∗ a l l v a l u e s i n i t a l l y 0 (FALSE) ∗/
16
17
18
19
20
}
21
void e n t e r r e g i o n ( i n t p r o c e s s )
/∗ p r o c e s s i s 0 o r 1 ∗/
{
int other ;
/∗ number o f t h e o t h e r p r o c e s s ∗/
o t h e r = 1− p r o c e s s ;
/∗ t h e o p p o s i t e o f p r o c e s s ∗/
interested [ process ]
/∗ show t h a t you a r e i n t e r e s t e d ∗/
turn = p r o c e s s ;
/∗ s e t f l a g ∗/
while ( t u r n == p r o c e s s && i n t e r e s t e d [ o t h e r ] == TRUE) ; /∗ n u l l
s t a t e m e n t ∗/
void l e a v e r e g i o n ( i n t p r o c e s s )
{
i n t e r e s t e d [ p r o c e s s ] = FALSE ;
c r i t i c a l r e g i o n ∗/
}
A.3.
/∗ p r o c e s s : who i s l e a v i n g ∗/
/∗ i n d i c a t e s d e p a r t u r e from
Solución propuesta por el creador del PlasmaOS
Aseguro que no halla más de un proceso en una regione crı́tica y pero no
se puede asegurar nada sobre su rendimiento y tampoco de que no caiga en un
deadlock. Un ejemplo propuesto es el siguiente:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
u i n t 3 2 OS SpinLock ( void )
{
u i n t 3 2 s t a t e , cpuIndex , i , ok , d e l a y ;
v o l a t i l e u i n t 3 2 keepVar ;
cpuIndex = OS CpuIndex ( ) ;
s t a t e = OS A sm I nt er r up t En ab l e ( 0 ) ;
i f ( SpinLockArray [ cpuIndex ] )
return ( u i n t 3 2 ) −1;
d e l a y = ( 4 + cpuIndex ) << 2 ;
// d i s a b l e i n t e r r u p t s
// a l r e a d y l o c k e d
// Spin u n t i l o n l y t h i s CPU has t h e s p i n l o c k
for ( ; ; )
{
ok = 1 ;
SpinLockArray [ cpuIndex ] = 1 ;
APÉNDICE A. IMPLEMENTACIONES DE SPINLOCKS
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
f o r ( i = 0 ; i < OS CPU COUNT; ++i )
{
i f ( i != cpuIndex && SpinLockArray [ i ] )
ok = 0 ;
//Another CPU has t h e s p i n l o c k
}
i f ( ok )
return s t a t e ;
SpinLockArray [ cpuIndex ] = 0 ;
OS A sm I nt er r up tE n ab l e ( s t a t e ) ;
// re −e n a b l e i n t e r r u p t s
f o r ( i = 0 ; i < d e l a y ; ++i )
// w a i t a b i t
++ok ;
keepVar = ok ;
//don ’ t o p t i m i z e away t h e d e l a y l o o p
i f ( delay < 128)
d e l a y <<= 1 ;
s t a t e = OS A sm I nt e rr up t En ab l e ( 0 ) ; // d i s a b l e i n t e r r u p t s
}
}
v o i d OS SpinUnlock ( u i n t 3 2 s t a t e )
{
u i n t 3 2 cpuIndex ;
i f ( s t a t e == ( u i n t 3 2 ) −1)
return ;
cpuIndex = OS CpuIndex ( ) ;
SpinLockArray [ cpuIndex ] = 0 ;
}
// n e s t e d l o c k c a l l
81
Descargar