resumen: encontrar y explotar el paralelismo (nivel de instrucción) en un programa de subproceso único se realiza únicamente en hardware, por el núcleo de la CPU en el que se ejecuta. Y solo a través de una ventana de un par de cientos de instrucciones, no reordenamiento a gran escala.
Los programas de un solo subproceso no obtienen ningún beneficio de las CPU de varios núcleos, excepto que otras cosas pueden ejecutarse en los otros núcleos en lugar de quitarle tiempo a la tarea de un solo subproceso.
El sistema operativo organiza las instrucciones de todos los subprocesos de tal manera que no se estén esperando el uno al otro.
El sistema operativo NO mira dentro de las secuencias de instrucciones de subprocesos. Solo programa hilos a núcleos.
En realidad, cada núcleo ejecuta la función del planificador del sistema operativo cuando necesita averiguar qué hacer a continuación. La programación es un algoritmo distribuido. Para comprender mejor las máquinas multinúcleo, piense que cada núcleo ejecuta el núcleo por separado. Al igual que un programa de subprocesos múltiples, el núcleo está escrito de modo que su código en un núcleo pueda interactuar de manera segura con su código en otros núcleos para actualizar las estructuras de datos compartidos (como la lista de subprocesos que están listos para ejecutarse.
De todos modos, el sistema operativo está involucrado en ayudar a los procesos de subprocesos múltiples a explotar el paralelismo a nivel de subproceso que debe exponerse explícitamente escribiendo manualmente un programa de subprocesos múltiples . (O mediante un compilador de paralelización automática con OpenMP o algo así).
Luego, el front-end de la CPU organiza aún más esas instrucciones distribuyendo un subproceso a cada núcleo y distribuye instrucciones independientes de cada subproceso entre los ciclos abiertos.
Un núcleo de la CPU solo ejecuta una secuencia de instrucciones, si no se detiene (dormido hasta la próxima interrupción, por ejemplo, la interrupción del temporizador). A menudo es un subproceso, pero también podría ser un controlador de interrupción del kernel o un código de kernel misceláneo si el kernel decidió hacer algo más que simplemente regresar al subproceso anterior después de manejar e interrumpir o llamar al sistema.
Con HyperThreading u otros diseños SMT, un núcleo de CPU físico actúa como múltiples núcleos "lógicos". La única diferencia desde la perspectiva del sistema operativo entre una CPU quad-core-with-hyperthreading (4c8t) y una máquina simple de 8 núcleos (8c8t) es que un sistema operativo compatible con HT intentará programar subprocesos para separar los núcleos físicos para que no No compiten entre ellos. Un sistema operativo que no sabía sobre hyperthreading solo vería 8 núcleos (a menos que desactive HT en el BIOS, entonces solo detectaría 4).
El término " front-end" se refiere a la parte de un núcleo de CPU que obtiene código de máquina, decodifica las instrucciones y las emite en la parte fuera de servicio del núcleo . Cada núcleo tiene su propio front-end, y es parte del núcleo como un todo. Las instrucciones que obtiene son lo que la CPU está ejecutando actualmente.
Dentro de la parte fuera de orden del núcleo, las instrucciones (o uops) se envían a los puertos de ejecución cuando sus operandos de entrada están listos y hay un puerto de ejecución libre. Esto no tiene que suceder en el orden del programa, así es como una CPU OOO puede explotar el paralelismo a nivel de instrucción dentro de un solo hilo .
Si reemplaza "núcleo" con "unidad de ejecución" en su idea, está cerca de corregir. Sí, la CPU distribuye instrucciones / uops independientes a las unidades de ejecución en paralelo. (Pero hay una confusión de terminología, ya que dijiste "front-end" cuando realmente es el programador de instrucciones de la CPU, también conocido como Reservation Station, que selecciona las instrucciones listas para ejecutar).
La ejecución fuera de orden solo puede encontrar ILP en un nivel muy local, solo hasta un par de cientos de instrucciones, no entre dos bucles independientes (a menos que sean cortos).
Por ejemplo, el equivalente asm de este
int i=0,j=0;
do {
i++;
j++;
} while(42);
funcionará casi tan rápido como el mismo bucle, incrementando solo un contador en Intel Haswell. i++
solo depende del valor anterior de i
, mientras que j++
solo depende del valor anterior de j
, por lo que las dos cadenas de dependencia pueden ejecutarse en paralelo sin romper la ilusión de que todo se ejecuta en el orden del programa.
En x86, el bucle se vería así:
top_of_loop:
inc eax
inc edx
jmp .loop
Haswell tiene 4 puertos de ejecución de enteros, y todos ellos tienen unidades sumadoras, por lo que puede soportar un rendimiento de hasta 4 inc
instrucciones por reloj si son independientes. (Con latencia = 1, solo necesita 4 registros para maximizar el rendimiento manteniendo 4 inc
instrucciones en vuelo. Compare esto con el vector-FP MUL o FMA: latencia = 5 rendimiento = 0.5 necesita 10 acumuladores de vectores para mantener 10 FMA en vuelo para maximizar el rendimiento. Y cada vector puede ser 256b, con 8 flotadores de precisión simple).
La rama tomada también es un cuello de botella: un bucle siempre toma al menos un reloj completo por iteración, porque el rendimiento de la rama tomada está limitado a 1 por reloj. Podría poner una instrucción más dentro del ciclo sin reducir el rendimiento, a menos que también lea / escriba eax
o edx
en cuyo caso alargaría esa cadena de dependencia. Poner 2 instrucciones más en el bucle (o una instrucción compleja multi-uop) crearía un cuello de botella en el front-end, ya que solo puede emitir 4 uops por reloj en el núcleo fuera de servicio. (Consulte estas preguntas y respuestas sobre SO para obtener algunos detalles sobre lo que sucede con los bucles que no son múltiplos de 4 uops: el bucle de bucle y el caché de uop hacen que las cosas sean interesantes)
En casos más complejos, encontrar el paralelismo requiere mirar una ventana más grande de instrucciones . (por ejemplo, tal vez hay una secuencia de 10 instrucciones que dependen unas de otras, luego algunas independientes).
La capacidad del búfer de reordenamiento es uno de los factores que limita el tamaño de la ventana fuera de orden. En Intel Haswell, son 192 uops. (E incluso puede medirlo experimentalmente , junto con la capacidad de cambio de nombre de registro (tamaño de archivo de registro)). Los núcleos de CPU de baja potencia como ARM tienen tamaños de ROB mucho más pequeños, si es que se ejecutan fuera de orden.
También tenga en cuenta que las CPU deben ser canalizadas, así como fuera de servicio. Por lo tanto, tiene que buscar y decodificar las instrucciones mucho antes de las que se están ejecutando, preferiblemente con un rendimiento suficiente para rellenar las memorias intermedias después de perder cualquier ciclo de recuperación. Las ramas son complicadas, porque no sabemos de dónde buscarlas si no sabemos en qué dirección se fue una rama. Es por eso que la predicción de ramas es tan importante. (Y por qué las CPU modernas usan ejecución especulativa: adivinan en qué dirección irá una rama y comienzan a buscar / decodificar / ejecutar ese flujo de instrucciones. Cuando se detecta una predicción errónea, vuelven al último estado bueno conocido y se ejecutan desde allí).
Si desea leer más sobre los componentes internos de la CPU, hay algunos enlaces en el wiki de la etiqueta Stackoverflow x86 , incluida la guía de microarquitectura de Agner Fog y las descripciones detalladas de David Kanter con diagramas de CPU de Intel y AMD. De su informe de microarquitectura Intel Haswell , este es el diagrama final de toda la tubería de un núcleo Haswell (no todo el chip).
Este es un diagrama de bloques de un solo núcleo de CPU . Una CPU de cuatro núcleos tiene 4 de estos en un chip, cada uno con sus propios cachés L1 / L2 (compartiendo un caché L3, controladores de memoria y conexiones PCIe a los dispositivos del sistema).
Sé que esto es abrumadoramente complicado. El artículo de Kanter también muestra partes de esto para hablar sobre la interfaz por separado de las unidades de ejecución o los cachés, por ejemplo.