69

He intentado optimizar un código extremadamente preciso para el rendimiento (un algoritmo de ordenamiento rápido que se llama millones y millones de veces dentro de una simulación de monte carlo) al desenrollarlo. Aquí está el bucle interno que estoy tratando de acelerar:¿Cuándo, si alguna vez, el bucle sigue siendo útil?

// Search for elements to swap. 
while(myArray[++index1] < pivot) {} 
while(pivot < myArray[--index2]) {} 

me trataron desenrollando a algo como:

while(true) { 
    if(myArray[++index1] < pivot) break; 
    if(myArray[++index1] < pivot) break; 
    // More unrolling 
} 


while(true) { 
    if(pivot < myArray[--index2]) break; 
    if(pivot < myArray[--index2]) break; 
    // More unrolling 
} 

Esto hizo absolutamente ninguna diferencia, así que cambió de nuevo a la forma más fácil de leer. He tenido experiencias similares otras veces que he intentado desenrollar loop. Dada la calidad de los predictores de bifurcaciones en el hardware moderno, ¿cuándo, si alguna vez, el bucle se está desenrollando sigue siendo una optimización útil?

+1

¿Puedo preguntar por qué no está utilizando las rutinas de biblioteca estándar de la biblioteca rápida? –

+8

@Poita: Porque los míos tienen algunas características adicionales que necesito para los cálculos estadísticos que estoy haciendo y están muy afinados para mis casos de uso y, por lo tanto, son menos generales pero más rápidos que la lib estándar. Estoy usando el lenguaje de programación D, que tiene un viejo optimizador de mierda, y para grandes matrices de flotantes aleatorias, aún así vencí al género C++ STL de GCC en un 10-20%. – dsimcha

Respuesta

90

El desenrollado de bucles tiene sentido si puede romper las cadenas de dependencia. Esto da a una CPU fuera de servicio o súper escalar la posibilidad de programar las cosas mejor y por lo tanto correr más rápido.

Un ejemplo simple:

for (int i=0; i<n; i++) 
{ 
    sum += data[i]; 
} 

Aquí la cadena de dependencias de los argumentos es muy corto. Si obtienes un puesto porque tienes un error de caché en la matriz de datos, la CPU no puede hacer nada más que esperar.

Por otro lado este código:

for (int i=0; i<n; i+=4) 
{ 
    sum1 += data[i+0]; 
    sum2 += data[i+1]; 
    sum3 += data[i+2]; 
    sum4 += data[i+3]; 
} 
sum = sum1 + sum2 + sum3 + sum4; 

podría correr más rápido. Si obtienes un error de caché u otro puesto en un cálculo, existen otras tres cadenas de dependencia que no dependen del puesto. Una CPU fuera de servicio puede ejecutar estos.

+2

Gracias. He intentado desenrollar loop en este estilo en otros lugares de la biblioteca donde estoy calculando sumas y cosas, y en estos lugares funciona de maravilla. Estoy casi seguro de que la razón es que aumenta el paralelismo del nivel de instrucción, como usted sugiere. – dsimcha

+2

Buena respuesta y ejemplo instructivo. Aunque no veo cómo los bloqueos en fallas de caché pueden afectar el rendimiento * de este ejemplo en particular *. Llegué a explicarme las diferencias de rendimiento entre los dos códigos (en mi máquina, el segundo fragmento de código es 2-3 veces más rápido) al observar que el primero desactiva cualquier tipo de paralelismo de nivel de instrucción en los carriles de coma flotante. El segundo permitiría que una CPU súper escalar ejecutara hasta cuatro puntos de coma flotante al mismo tiempo. –

+1

Tenga en cuenta que el resultado no será numéricamente idéntico al bucle original al calcular una suma de esta manera. – Barabas

17

Eso no haría ninguna diferencia porque estás haciendo el mismo número de comparaciones. Aquí hay un mejor ejemplo. En lugar de:

for (int i=0; i<200; i++) { 
    doStuff(); 
} 

escritura:

for (int i=0; i<50; i++) { 
    doStuff(); 
    doStuff(); 
    doStuff(); 
    doStuff(); 
} 

Incluso entonces es casi seguro que no importa, sino que ahora está haciendo comparaciones en lugar de 50 200 (imaginar la comparación es más complejo).

Manual desenrollar bucle en general es en gran medida un artefacto de la historia sin embargo. Es otra de la creciente lista de cosas que un buen compilador hará por ti cuando sea importante. Por ejemplo, la mayoría de las personas no se molestan en escribir x << 1 o x += x en lugar de x *= 2. Simplemente escriba x *= 2 y el compilador lo optimizará para lo que sea mejor.

Básicamente cada vez hay menos necesidad de adivinar su compilador.

+0

Estoy de acuerdo, esos días han terminado en los que puede ajustar algún ciclo aquí y allá y esperar un gran beneficio. Los compiladores están tan avanzados. – fastcodejava

+0

Me gusta cuando el compilador optimiza 'x * = 2' para mí. No me gusta cuando intenta reorganizar mi código. Eso incluye desenrollar bucles, levantar códigos, eludir el código que cree que nunca se alcanzará, cosas así. Soy perfectamente capaz de decidir cuándo o cuándo no hacer esas cosas. –

+1

@Mike Definitivamente desactivando la optimización si es una buena idea cuando se desconcierta, pero vale la pena leer el enlace que publicó Poita_. Los compiladores son * dolorosamente * buenos en ese negocio. – dmckee

0

El desenrollado del lazo depende completamente del tamaño de su problema. Es completamente dependiente de que su algoritmo sea capaz de reducir el tamaño en grupos de trabajo más pequeños. Lo que hiciste arriba no se ve así. No estoy seguro si una simulación de monte carlo puede incluso desenrollarse.

I buen escenario para desenrollar el bucle sería rotar una imagen. Dado que podría rotar grupos de trabajo separados. Para que esto funcione, debería reducir el número de iteraciones.

+0

Estaba desenrollando un tipo rápido que se llama desde el bucle interno de mi simulación, no el bucle principal de la simulación. – dsimcha

13

Independientemente de la predicción de bifurcación en hardware moderno, la mayoría de los compiladores se desenrollan para usted de todos modos.

Valdría la pena averiguar cuántas optimizaciones hace su compilador para usted.

Encontré Felix von Leitner's presentation muy esclarecedor sobre el tema. Te recomiendo que lo leas. Resumen: los compiladores modernos son MUY inteligentes, por lo que las optimizaciones de mano casi nunca son efectivas.

+0

Agradable leer. Gracias. – dsimcha

+6

Esa es una buena lectura, pero la única parte que pensé que estaba en la mira fue cuando habla de mantener la estructura de datos simple. El resto fue preciso, pero descansa en una suposición gigante no enunciada: que lo que se está ejecutando * tiene * que ser. En la afinación que hago, encuentro a personas preocupadas por los registros y las fallas de la memoria caché cuando grandes cantidades de tiempo van a montañas innecesarias de código de abstracción. –

+0

"las optimizaciones de mano casi nunca son efectivas" → Tal vez sea cierto si es completamente nuevo en la tarea. Simplemente no es cierto de lo contrario. – Veedrac

0

El desenrollado del bucle sigue siendo útil si hay muchas variables locales tanto dentro como con el bucle. Para reutilizar esos registros más en lugar de guardar uno para el índice de bucle.

En su ejemplo, utiliza una pequeña cantidad de variables locales, sin sobreutilizar los registros.

La comparación (al final del bucle) también es un inconveniente importante si la comparación es pesada (es decir, no instrucciones test), especialmente si depende de una función externa.

El desenrollado del bucle también ayuda a aumentar el conocimiento de la CPU para la predicción de bifurcación, pero eso ocurre de todos modos.

2

Por lo que yo entiendo, los compiladores modernos ya desenrollar bucles en su caso - un ejemplo es gcc, si se aprueba los parámetros de optimización que el manual dice que:

Desenrollar bucles cuyo número de iteraciones puede se determinará en tiempo de compilación o al ingresar al bucle .

Por lo tanto, en la práctica, es probable que su compilador le haga los casos más triviales. Depende de usted, por lo tanto, asegurarse de que el número de bucles sea lo más fácil posible para que el compilador determine cuántas iteraciones se necesitarán.

+0

Justo a tiempo, los compiladores generalmente no se desenrollan, las heurísticas son demasiado costosas. Los compiladores estáticos pueden dedicarle más tiempo, pero la diferencia entre las dos formas dominantes es importante. – Abel

2

El desenrollado en bucle, ya sea desenrollando las manos o desenrollando el compilador, a menudo puede ser contraproducente, especialmente con las CPU x86 más recientes (Core 2, Core i7). En pocas palabras: compare su código con y sin bucle en cualquiera de las CPU en las que planea implementar este código.

+0

¿Por qué particularmente en CPUs x86 recet? – JohnTortugo

+3

@JohnTortugo: las CPU x86 modernas tienen ciertas optimizaciones para bucles pequeños; consulte p. Ej. Loop Stream Detector en Core y Nehalem Achitectures: desenrollar un bucle para que ya no sea lo suficientemente pequeño como para caber en el caché de LSD, derrota esta optimización. Ver p. http://www.tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html –

1

Probar sin saber no es la manera de hacerlo.
¿Este tipo toma un alto porcentaje del tiempo total?

Todo lo que se desenrolla de bucle es reducir la sobrecarga de bucle de incrementar/disminuir, comparando para la condición de parada y saltando. Si lo que está haciendo en el ciclo requiere más ciclos de instrucción que la sobrecarga del ciclo en sí, no verá una gran mejora porcentual.

Here's an example of how to get maximum performance.

1

Loop desenrollar puede ser útil en casos específicos. ¡La única ganancia es no saltearse algunas pruebas!

Puede, por ejemplo, permitir el reemplazo escalar, la inserción eficiente de la recuperación previa del software ... Se sorprendería de lo útil que puede ser (puede obtener aceleración del 10% en la mayoría de los bucles incluso con -O3) desenrollando agresivamente.

Como se dijo antes, depende mucho del ciclo y el compilador y el experimento son necesarios. Es difícil hacer una regla (o la heurística del compilador para desenrollar sería perfecta)

Cuestiones relacionadas