En informática , la programación de instrucciones es una optimización del compilador que se utiliza para mejorar el paralelismo a nivel de instrucción , lo que mejora el rendimiento en máquinas con secuencias de instrucciones . En términos más simples, intenta hacer lo siguiente sin cambiar el significado del código:
Los bloqueos de la tubería pueden ser causados por peligros estructurales (límite de recursos del procesador), peligros de datos (salida de una instrucción necesaria para otra instrucción) y peligros de control (ramificación).
La programación de instrucciones se realiza normalmente en un único bloque básico . Para determinar si la reorganización de las instrucciones del bloque de una determinada manera preserva el comportamiento de ese bloque, necesitamos el concepto de dependencia de datos . Existen tres tipos de dependencias, que también son los tres peligros de los datos :
Técnicamente, existe un cuarto tipo, Lectura tras lectura (RAR o "Input"): ambas instrucciones leen la misma ubicación. La dependencia de entrada no restringe el orden de ejecución de dos instrucciones, pero es útil en el reemplazo escalar de elementos de matriz.
Para asegurarnos de respetar los tres tipos de dependencias, construimos un gráfico de dependencia, que es un gráfico dirigido donde cada vértice es una instrucción y hay un borde de I 1 a I 2 si I 1 debe venir antes que I 2 debido a una dependencia. Si se omiten las dependencias llevadas a cabo en bucle, el gráfico de dependencia es un gráfico acíclico dirigido . Entonces, cualquier ordenación topológica de este gráfico es una programación de instrucciones válida. Los bordes del gráfico suelen estar etiquetados con la latencia de la dependencia. Este es el número de ciclos de reloj que deben transcurrir antes de que la secuencia pueda continuar con la instrucción de destino sin detenerse.
El algoritmo más simple para encontrar una clasificación topológica se utiliza con frecuencia y se conoce como programación de listas . Conceptualmente, selecciona repetidamente una fuente del gráfico de dependencia, la agrega a la programación de instrucciones actual y la elimina del gráfico. Esto puede hacer que otros vértices sean fuentes, que luego también se considerarán para la programación. El algoritmo finaliza si el gráfico está vacío.
Para lograr una buena programación, se deben evitar los bloqueos. Esto se determina mediante la elección de la siguiente instrucción que se va a programar. Se utilizan varias heurísticas:
La programación de instrucciones se puede realizar antes o después de la asignación de registros , o bien antes y después de ella. La ventaja de hacerlo antes de la asignación de registros es que esto da como resultado un paralelismo máximo. La desventaja de hacerlo antes de la asignación de registros es que esto puede dar como resultado que el asignador de registros necesite utilizar una cantidad de registros que exceda los disponibles. Esto provocará que se introduzca código de relleno/desbordamiento, lo que reducirá el rendimiento de la sección de código en cuestión.
Si la arquitectura que se está programando tiene secuencias de instrucciones que tienen combinaciones potencialmente ilegales (debido a la falta de interbloqueos de instrucciones), las instrucciones deben programarse después de la asignación de registros. Este segundo paso de programación también mejorará la ubicación del código de derrame/relleno.
Si la programación solo se realiza después de la asignación de registros, entonces habrá dependencias falsas introducidas por la asignación de registros que limitarán la cantidad de movimiento de instrucciones posible por parte del programador.
Existen varios tipos de programación de instrucciones:
GNU Compiler Collection es un compilador conocido por realizar la programación de instrucciones, utilizando los indicadores -march
(conjunto de instrucciones y programación) o -mtune
(solo programación). Utiliza descripciones de latencias de instrucciones y qué instrucciones se pueden ejecutar en paralelo (o equivalentemente, qué "puerto" utiliza cada una) para cada microarquitectura para realizar la tarea. Esta característica está disponible para casi todas las arquitecturas que admite GCC. [2]
Hasta la versión 12.0.0, la programación de instrucciones en LLVM /Clang solo podía aceptar un conmutador -march
(llamado target-cpu
en el lenguaje de LLVM) tanto para el conjunto de instrucciones como para la programación. La versión 12 agrega soporte para -mtune
( tune-cpu
) solo para x86. [3]
Las fuentes de información sobre latencia y uso del puerto incluyen:
Los LLVM llvm-exegesis
deberían poder utilizarse en todas las máquinas, especialmente para recopilar información en aquellas que no sean x86. [6]