El desenrollado de bucles , también conocido como desenrollado de bucles , es una técnica de transformación de bucles que intenta optimizar la velocidad de ejecución de un programa a costa de su tamaño binario , un enfoque conocido como compensación espacio-tiempo . La transformación puede realizarse manualmente por el programador o mediante un compilador optimizador . En los procesadores modernos, el desenrollado de bucles suele ser contraproducente, ya que el aumento del tamaño del código puede provocar más fallos de caché; véase el dispositivo de Duff . [ 1 ]
El objetivo del desenrollado de bucles es aumentar la velocidad de un programa reduciendo o eliminando las instrucciones que controlan el bucle, como la aritmética de punteros y las pruebas de "fin de bucle" en cada iteración; [ 2 ] reduciendo las penalizaciones de bifurcación; así como ocultando las latencias, incluido el retraso en la lectura de datos de la memoria. [ 3 ] Para eliminar esta sobrecarga computacional , los bucles se pueden reescribir como una secuencia repetida de instrucciones independientes similares. [ 4 ]
El desenrollado de bucles también forma parte de ciertas técnicas de verificación formal , en particular la verificación de modelos acotada . [ 5 ]
Ventajas
La sobrecarga en los bucles "ajustados" suele consistir en instrucciones para incrementar un puntero o índice al siguiente elemento de un array ( aritmética de punteros ), así como en comprobaciones de "fin de bucle". Si un compilador o ensamblador optimizador puede precalcular los desplazamientos a cada variable de array referenciada individualmente , estos se pueden integrar directamente en las instrucciones del código máquina , eliminando así la necesidad de operaciones aritméticas adicionales en tiempo de ejecución.
- Se pueden obtener mejoras significativas si la reducción en las instrucciones ejecutadas compensa cualquier disminución del rendimiento causada por un aumento en el tamaño del programa.
- La penalización por bifurcación se minimiza. [ 6 ]
- Si las instrucciones dentro del bucle son independientes entre sí (es decir, si las instrucciones que aparecen antes en el bucle no afectan a las que les siguen), potencialmente pueden ejecutarse en paralelo .
- Se puede implementar dinámicamente si se desconoce el número de elementos de la matriz en tiempo de compilación (como en el dispositivo de Duff ).
En ocasiones, los compiladores optimizadores realizan el desenrollado automáticamente o bajo petición.
Desventajas
- Aumento del tamaño del código: El desenrollado aumenta el número de instrucciones, lo que da como resultado archivos binarios de programa más grandes.
- Mayores requisitos de almacenamiento: El código ampliado ocupa más memoria, lo que puede resultar problemático para microcontroladores o sistemas embebidos con almacenamiento limitado.
- Presión sobre la caché de instrucciones : El bucle desenrollado consume más espacio en la caché de instrucciones. Si supera el tamaño de la caché, pueden producirse fallos de caché frecuentes, lo que puede provocar una grave degradación del rendimiento debido a los costosos accesos a la memoria.
- Menor legibilidad del código : si el desenrollado de bucles se realiza manualmente en lugar de mediante un compilador optimizador, el código puede resultar más difícil de entender y mantener.
- Conflicto con la inserción de funciones : Cuando el cuerpo del bucle contiene llamadas a funciones, el desenrollado puede impedir la inserción de funciones debido a la expansión excesiva del código, lo que conlleva una compensación entre estas dos optimizaciones.
- Mayor presión sobre los registros : En hardware que depende del procesamiento en paralelo de software para el rendimiento (por ejemplo, sistemas sin cambio de nombre de registros o con ejecución superescalar en orden ), el desenrollado puede requerir registros adicionales para almacenar variables temporales entre iteraciones, lo que limita la reutilización de registros. [ 7 ]
- Predicción de bifurcaciones: Las CPU modernas utilizan la predicción de bifurcaciones para intentar adivinar hacia dónde se dirigirá una bifurcación. Si la predicción es correcta, la CPU puede continuar ejecutando instrucciones sin esperar a que la bifurcación se resuelva. Sin embargo, si la predicción es incorrecta, la CPU tiene que vaciar la tubería y comenzar a ejecutar las instrucciones correctas, lo que puede suponer una penalización en el rendimiento. El desenrollado de bucles puede aumentar el número de bifurcaciones en el código, lo que podría provocar más predicciones erróneas de bifurcaciones y un menor rendimiento. [ 8 ]
Desenrollado de bucle estático/manual
El desenrollado manual (o estático) de bucles implica que el programador analice el bucle e interprete las iteraciones en una secuencia de instrucciones que reduzca la sobrecarga del bucle. Esto contrasta con el desenrollado dinámico, que lo realiza el compilador.
Ejemplo manual sencillo en C
Un procedimiento en un programa informático consiste en eliminar 100 elementos de una colección. Esto se suele lograr mediante un forbucle que llama a la función remove(item_number) . Si se desea optimizar esta parte del programa, y la sobrecarga del bucle requiere muchos recursos en comparación con los de la función remove(x) , se puede utilizar el desenrollado del bucle para acelerarlo.
Como resultado de esta modificación, el nuevo programa solo necesita realizar 20 iteraciones en lugar de 100. Posteriormente, solo se requiere el 20 % de los saltos y bifurcaciones condicionales, lo que representa, a lo largo de muchas iteraciones, una reducción potencialmente significativa en la sobrecarga de administración del bucle. Para obtener el máximo beneficio, no se deben especificar variables en el código desplegado que requieran aritmética de punteros . Esto generalmente requiere direccionamiento de " base más desplazamiento", en lugar de referencias indexadas.
Por otro lado, este desenrollado manual del bucle aumenta el tamaño del código fuente de 3 a 7 líneas, que deben producirse, verificarse y depurarse, y el compilador puede tener que asignar más registros para almacenar variables en la iteración del bucle expandido . Además, las variables de control del bucle y el número de operaciones dentro de la estructura del bucle desenrollado deben elegirse cuidadosamente para que el resultado sea realmente el mismo que en el código original (suponiendo que se trata de una optimización posterior sobre código que ya funciona). Por ejemplo, considérense las implicaciones si el número de iteraciones no fuera divisible por 5. Las modificaciones manuales requeridas también se vuelven algo más complicadas si las condiciones de prueba son variables. Véase también el dispositivo de Duff .
Complejidad temprana
En el caso simple, el control de bucle es simplemente una sobrecarga administrativa que organiza las instrucciones productivas. El bucle en sí no contribuye en nada a los resultados deseados, simplemente le ahorra al programador el tedio de replicar el código cien veces, lo cual podría haberse hecho mediante un preprocesador que generara las replicaciones o un editor de texto. De manera similar, iflas instrucciones y otras instrucciones de control de flujo podrían reemplazarse por la replicación de código, excepto que el resultado puede ser un código inflado . Los programas informáticos rastrean fácilmente las combinaciones, pero a los programadores les resulta aburrida esta repetición y cometen errores. Considere:
Pero, por supuesto, el código ejecutado no tiene por qué ser la invocación de un procedimiento, y el siguiente ejemplo involucra la variable de índice en el cálculo:
que, si se compila, podría producir mucho código ( las instrucciones print son notorias), pero es posible una mayor optimización. Este ejemplo solo hace referencia a x(i) y x(i - 1) en el bucle (este último solo para desarrollar el nuevo valor x(i) ); por lo tanto, dado que no hay ninguna referencia posterior al array x desarrollado aquí, sus usos podrían reemplazarse por una variable simple. Sin embargo, tal cambio significaría una variable simple cuyo valor cambia, mientras que si se mantiene el array, el análisis del compilador podría notar que los valores del array son constantes, cada uno derivado de una constante anterior, y por lo tanto lleva adelante los valores constantes para que el código se convierta en
imprimir 2, 2; imprimir 3, 6; impresión 4, 24; ...etc.
En general, el contenido de un bucle puede ser extenso e implicar una indexación compleja de matrices. Probablemente, estos casos se gestionan mejor mediante compiladores optimizadores. Replicar los bucles internos permite muchas optimizaciones posibles, pero la mejora es mínima a menos que n sea grande.
Desenrollar bucles WHILE
Consideremos un bucle WHILE en pseudocódigo similar al siguiente:
En este caso, el desenrollado es más rápido porque el ENDWHILE (un salto al inicio del bucle) se ejecutará un 66% menos a menudo.
Mejor aún, el ejemplo de pseudocódigo "modificado", que algunos compiladores optimizadores pueden ejecutar automáticamente, elimina por completo los saltos incondicionales.
Despliegue dinámico
Dado que las ventajas del desenrollado de bucles suelen depender del tamaño de un array —que a menudo se desconoce hasta el tiempo de ejecución— , los compiladores JIT (por ejemplo) pueden determinar si invocar una secuencia de bucle "estándar" o generar una secuencia (relativamente corta) de instrucciones individuales para cada elemento. Esta flexibilidad es una de las ventajas de las técnicas de compilación justo a tiempo frente a la optimización estática o manual en el contexto del desenrollado de bucles. En esta situación, a menudo es con valores relativamente pequeños de n donde el ahorro sigue siendo útil , requiriendo un aumento mínimo (o nulo) en el tamaño total del programa (que podría incluirse solo una vez, como parte de una biblioteca estándar).
Los programadores de lenguaje ensamblador (incluidos los desarrolladores de compiladores optimizadores) también pueden beneficiarse de la técnica de desenrollado dinámico de bucles, utilizando un método similar al empleado para tablas de ramificación eficientes . En este caso, la ventaja es mayor cuando el desplazamiento máximo de cualquier campo referenciado en una matriz determinada es menor que el desplazamiento máximo que se puede especificar en una instrucción de máquina (el ensamblador lo detectará si se supera).
Ejemplo de ensamblador (IBM/360 o Z/Architecture)
Este ejemplo es para ensambladores IBM/360 o Z/Architecture y supone que se va a copiar un campo de 100 bytes (en el desplazamiento cero) de la matriz FROM a la matriz TO , ambas con 50 entradas con longitudes de elemento de 256 bytes cada una.
* La dirección de devolución se encuentra en R14. * Inicialice los registros R15, R0, R1 y R2 a partir de los datos definidos al final de * el programa que comienza con la etiqueta INIT/MAXM1. LM R15,R2,INIT Establecer R15 = número máximo de MVC * instrucciones (MAXM1 = 16), * R0 = número de entradas de la matriz, * R1 = dirección del array 'FROM', y * R2 = dirección del array 'TO'. * * El bucle comienza aquí. LOOP EQU * Definir la etiqueta LOOP. * En este punto, R15 siempre contendrá el número 16 (MAXM1). SR R15,R0 Resta el número restante de * entradas en el array (R0) de R15. BNP TODOS Si R15 no es positivo, significa que * tener más de 16 entradas restantes * en el array, salta para hacer todo * Secuencia MVC y luego repetir. * * Calcular un desplazamiento (desde el inicio de la secuencia MVC) para la bifurcación incondicional a * el bucle MVC 'desenrollado' a continuación. * Si el número de entradas restantes en las matrices es cero, R15 será 16, por lo tanto * Se omitirán todas las instrucciones MVC. MH R15,=AL2(ILEN) Multiplica R15 por la longitud de uno * Instrucciones MVC. B ALL(R15) Saltar a ALL+R15, la dirección de la * instrucción MVC específica calculada * con caída a través del resto de ellos. * * Instrucción MVC 'tabla'. * La primera entrada tiene el desplazamiento máximo permitido con un solo registro = hexadecimal F00 * (15*256) en este ejemplo. * Las 16 instrucciones MVC ('mover personaje') siguientes utilizan base más desplazamiento. * El direccionamiento y cada desplazamiento hacia/desde disminuyen en la longitud de un elemento de la matriz. * (256). Esto evita que se requiera aritmética de punteros para cada elemento hasta un * Desplazamiento máximo permitido dentro de la instrucción FFF hexadecimal * (15*256+255). Las instrucciones están en orden de desplazamiento decreciente, por lo que la última * El elemento del conjunto se mueve primero. ALL MVC 15*256(100,R2),15*256(R1) Mover 100 bytes de la entrada número 16 desde * matriz 1 a matriz 2 (con * paso directo). ILEN EQU *-ALL Establece ILEN a la longitud del anterior * Instrucciones MVC. MVC 14*256(100,R2),14*256(R1) Mover 100 bytes de la entrada número 15. MVC 13*256(100,R2),13*256(R1) Mover 100 bytes de la entrada número 14. MVC 12*256(100,R2),12*256(R1) Mover 100 bytes de la entrada número 13. MVC 11*256(100,R2),11*256(R1) Mover 100 bytes de la entrada número 12. MVC 10*256(100,R2),10*256(R1) Mover 100 bytes de la entrada número 11. MVC 09*256(100,R2),09*256(R1) Mover 100 bytes de la décima entrada. MVC 08*256(100,R2),08*256(R1) Mover 100 bytes de la novena entrada. MVC 07*256(100,R2),07*256(R1) Mover 100 bytes de la octava entrada. MVC 06*256(100,R2),06*256(R1) Mover 100 bytes de la séptima entrada. MVC 05*256(100,R2),05*256(R1) Mover 100 bytes de la sexta entrada. MVC 04*256(100,R2),04*256(R1) Mover 100 bytes de la quinta entrada. MVC 03*256(100,R2),03*256(R1) Mover 100 bytes de la cuarta entrada. MVC 02*256(100,R2),02*256(R1) Mover 100 bytes de la tercera entrada. MVC 01*256(100,R2),01*256(R1) Mover 100 bytes de la segunda entrada. MVC 00*256(100,R2),00*256(R1) Mover 100 bytes de la primera entrada. * S R0,MAXM1 Reduzca el número de entradas restantes * para procesar. BNPR R14 Si no hay más entradas para procesar, devolver * para abordar en R14. AH R1,=AL2(16*256) Incrementar el puntero de matriz 'FROM' más allá * primer conjunto. AH R2,=AL2(16*256) Incrementa el puntero de matriz 'TO' más allá * primer conjunto. L R15,MAXM1 Recargar el número máximo de MVC * instrucciones por lote en R15 * (destruido por el cálculo en el * primera instrucción del bucle). B BUCLE Ejecutar el bucle de nuevo. * * Constantes y variables estáticas (estas podrían pasarse como parámetros, excepto * MAXM1). INIT DS 0A 4 direcciones (punteros) a ser * precargado con la instrucción 'LM' * al comienzo del programa. MAXM1 DC A(16) Número máximo de instrucciones MVC * ejecutado por lote. N DC A(50) Número de entradas reales en la matriz (a * variable, definida en otro lugar). DC A(DESDE) Dirección de inicio de la matriz 1 * ("puntero"). DC A(TO) Dirección de inicio de la matriz 2 * ("puntero"). * * Matrices estáticas (estas podrían adquirirse dinámicamente). FROM DS 50CL256 Matriz de 50 entradas de 256 bytes cada una. A DS 50CL256 Matriz de 50 entradas de 256 bytes cada una. En este ejemplo, se requerirían aproximadamente 202 instrucciones con un bucle "convencional" (50 iteraciones), mientras que el código dinámico anterior requeriría solo unas 89 instrucciones (lo que supone un ahorro de aproximadamente el 56%). Si el array hubiera tenido solo dos entradas, se ejecutaría en un tiempo similar al del bucle original desenrollado. El aumento en el tamaño del código es de tan solo unos 108 bytes , incluso si el array contiene miles de entradas.
Por supuesto, se pueden usar técnicas similares cuando hay varias instrucciones involucradas, siempre que la longitud combinada de las instrucciones se ajuste en consecuencia. Por ejemplo, en este mismo ejemplo, si se requiere borrar el resto de cada entrada de la matriz a nulos inmediatamente después de copiar el campo de 100 bytes, se puede agregar una instrucción de borrado adicional, , inmediatamente después de cada MVC en la secuencia (donde coincide con el valor en el MVC anterior).XC xx*256+100(156,R1),xx*256+100(R2)xx
Por supuesto, es perfectamente posible generar el código anterior "en línea" utilizando una sola macro de ensamblador , especificando solo cuatro o cinco operandos (o, alternativamente, convertirlo en una subrutina de biblioteca, a la que se accede mediante una simple llamada, pasando una lista de parámetros), lo que facilita la optimización.
Ejemplo en C
El siguiente ejemplo muestra el desenrollado dinámico de bucles para un programa sencillo escrito en C. A diferencia del ejemplo en ensamblador anterior, en este caso el compilador sigue generando aritmética de punteros e índices, ya que se sigue utilizando una variable (i) para acceder al elemento del array. La optimización completa solo es posible si se utilizan índices absolutos en las instrucciones de reemplazo.
#include <stdio.h>// El número de entradas procesadas por iteración del bucle. // Tenga en cuenta que este número es una 'constante constante' que refleja el código siguiente. constexpr int BUNCHSIZE = 8int main ( void ) { int i = 0 ; // contador int entries = 50 ; // número total a procesar // Si el número de elementos no es divisible por BUNCHSIZE, // se obtienen los tiempos de repetición necesarios para realizar la mayor parte del procesamiento en el bucle whileint repeat = ( entradas / TAMAÑO_BUNCH ); // número de veces que se repite int left = ( entradas % TAMAÑO_BUNCH ); // calcular el resto// Desenrollar el bucle en 'grupos' de 8 while ( repeat-- ) { printf ( "process(%d) \ n " , i ); printf ( "process(%d) \n " , i + 1 ); printf ( "process(%d) \n " , i + 2 ); printf ( "process(%d) \n " , i + 3 ); printf ( "process(%d) \n " , i + 4 ); printf ( "process(%d) \n " , i + 5 ); printf ( "process(%d) \n " , i + 6 ); printf ( "process(%d) \n " , i + 7 );// actualiza el índice por la cantidad procesada de una sola vez i += BUNCHSIZE ; }// Usa una instrucción switch para procesar lo restante saltando a la etiqueta del caso // en la etiqueta que luego pasará para completar el conjunto switch ( left ) { case 7 : printf ( "process(%d) \n " , i + 6 ); // procesar y confiar en el paso a través case 6 : printf ( "process(%d) \n " , i + 5 ); case 5 : printf ( "process(%d) \n " , i + 4 ); case 4 : printf ( "process(%d) \n " , i + 3 ); case 3 : printf ( "process(%d) \n " , i + 2 ); case 2 : printf ( "process(%d) \n " , i + 1 ); // quedan dos case 1 : printf ( "process(%d) \n " , i ); // solo queda uno por procesar case 0 : break ; // no queda ninguno } }La duplicación de código podría evitarse escribiendo las dos partes juntas, como en el dispositivo de Duff .
Ejemplo de desenrollado de bucle de C a lenguaje ensamblador MIPS
Fuente: [ 9 ]
El siguiente ejemplo calculará el producto escalar de dos vectores A y B de 100 entradas de tipo double. Aquí está el código en C:
doble producto escalar = 0 ;para ( int i = 0 ; i < 100 ; i ++ ) {producto escalar += A [ i ] * B [ i ];}Conversión a lenguaje ensamblador MIPS
A continuación se muestra un código ensamblador MIPS que calcula el producto escalar de dos vectores de 100 entradas, A y B, antes de implementar el desenrollado del bucle. El código que se muestra a continuación omite las inicializaciones del bucle:
- Inicializa el contador del bucle ($7) a 100.
- Inicializa el producto escalar ($f8) a 0.
- Inicializa
A[i]el puntero ($5) a la dirección base deA. - Inicializa
B[i]el puntero ($6) a la dirección base deB.
Tenga en cuenta que el tamaño de un elemento de las matrices (a double) es de 8 bytes.
bucle3:ld $f10 , 0 ( $5 ) ; $f10 ← A[yo]ld $f12 , 0 ( $6 ) ; $f12 ← B[i]mul.d $f10 , $f10 , $f12 ; $f10 ← A[i]*B[i]add.d $f8 , $f8 , $f10 ; $f8 ← $f8 + A[i]*B[i]addi $5 , $5 , 8 ; incrementa el puntero para A[i] en el tamaño; de un doble.addi $6 , $6 , 8 ; incrementa el puntero para B[i] en el tamaño; de un doble.agregar $7 , $7 , -1 ; decrementar el contador del bucleprueba:bgtz $7 , loop3 ; Continuar si el número de iteraciones es mayor que 0Desenrollando el bucle en MIPS
Lo siguiente es lo mismo que lo anterior, pero con el desenrollado del bucle implementado con un factor de 4. Nótese de nuevo que el tamaño de un elemento de los arreglos (a double) es de 8 bytes; por lo tanto, los desplazamientos 0, 8, 16, 24 y el desplazamiento 32 en cada bucle.
bucle3:ld $f10 , 0 ( $5 ) ; iteración con desplazamiento 0ld $f12 , 0 ( $6 )mul.d $f10 , $f10 , $f12agregar.d $f8 , $f8 , $f10ld $f10 , 8 ( $5 ) ; iteración con desplazamiento 8ld $f12 , 8 ( $6 )mul.d $f10 , $f10 , $f12agregar.d $f8 , $f8 , $f10ld $f10 , 16 ( $5 ) ; iteración con desplazamiento 16ld $f12 , 16 ( $6 )mul.d $f10 , $f10 , $f12agregar.d $f8 , $f8 , $f10ld $f10 , 24 ( $5 ) ; iteración con desplazamiento 24ld $f12 , 24 ( $6 )mul.d $f10 , $f10 , $f12agregar.d $f8 , $f8 , $f10agregar $5 , $5 , 32addi $6 , $6 , 32agregar $7 , $7 , -4prueba:bgtz $7 , loop3 ; Continuar el bucle si $7 > 0Véase también
Referencias
- ↑ Tso, Ted (22 de agosto de 2000). "Re: [ PATCH ] Re: Move of input drivers, some word needed from you" . lkml.indiana.edu . Linux kernel mailing list . Recuperado el 22 de agosto de 2014 .
Jim Gettys tiene una explicación maravillosa de este efecto en el servidor X. Resulta que con las predicciones de bifurcación y la velocidad relativa de la CPU frente a la memoria cambiando en la última década, el desenrollado de bucles es prácticamente inútil. De hecho, al eliminar todas las instancias de Duff's Device del servidor XFree86 4.0, el servidor se redujo en tamaño en _medio_ _megabyte_ (!!!), y fue más rápido para arrancar, porque la eliminación de todo ese código superfluo significó que el servidor X no estaba saturando tanto las líneas de caché.
- ↑ Ullman, Jeffrey D.; Aho, Alfred V. (1977). Principios del diseño de compiladores . Reading, Mass: Addison-Wesley Pub. Co. pp. 471–2 . ISBN 0-201-10073-8.
- ↑ Petersen, WP, Arbenz, P. (2004). Introducción a la computación paralela . Oxford University Press. pág. 10 .
{{cite book}}: CS1 maint: varios nombres: lista de autores ( enlace ) - ↑ Nicolau, Alexandru (1985). "Cuantización de bucles: desenrollado para la explotación del paralelismo de grano fino". Informe técnico del Departamento de Ciencias de la Computación. Ithaca, NY: Universidad de Cornell. OCLC 14638257 .
{{cite journal}}: Para citar una revista se requiere|journal=( ayuda ) - ↑ Verificación de modelos mediante SMT y teoría de listas
- ↑ Fog, Agner (29 de febrero de 2012). "Optimización de subrutinas en lenguaje ensamblador" (PDF) . Facultad de Ingeniería de la Universidad de Copenhague. pág. 100. Recuperado el 22 de septiembre de 2012.
12.11 Desenrollado de bucles
- ↑ Sarkar, Vivek (2001). "Desenrollado optimizado de bucles anidados". International Journal of Parallel Programming . 29 (5): 545– 581. doi : 10.1023/A:1012246031671 . S2CID 3353104 .
- ↑ Adam Horvath "Desenrollando el código: el rendimiento está muy lejos"
- ↑ "Desenrollado de bucle" . Universidad de Minnesota .
Lecturas adicionales
- Kennedy, Ken; Allen, Randy (2001). Optimizing Compilers for Modern Architectures: A Dependence-based Approach . Morgan Kaufmann. ISBN 1-55860-286-0.
Enlaces externos
- El capítulo 7, páginas 8 a 10 , del libro "Graphics Programming Black Book" de Michael Abrash trata sobre el desenrollado de bucles, con un ejemplo en lenguaje ensamblador x86.
- El libro "Desenrollado generalizado de bucles" ofrece una introducción concisa.
- Optimización de subrutinas en lenguaje ensamblador: Manual de optimización de Agner Fog con la técnica de desenrollado de bucles (2012).
- Optimizaciones del compilador
- Computación paralela