¿Cuándo, si alguna vez, sigue siendo útil el desenrollado de bucles?

93

He estado tratando de optimizar un código extremadamente crítico para el rendimiento (un algoritmo de clasificación rápida que se llama millones y millones de veces dentro de una simulación de monte carlo) mediante el desenrollado de bucle. Aquí está el bucle interno que estoy tratando de acelerar:

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

Intenté desenrollarlo 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 no hizo ninguna diferencia, así que lo cambié de nuevo a la forma más legible. He tenido experiencias similares otras veces que he intentado desenrollar bucles. Dada la calidad de los predictores de rama en el hardware moderno, ¿cuándo, si es que alguna vez, el desenrollado de bucles sigue siendo una optimización útil?

dsimcha
fuente
1
¿Puedo preguntarle por qué no está utilizando rutinas estándar de ordenación rápida de bibliotecas?
Peter Alexander
14
@Poita: Porque las mías tienen algunas características adicionales que necesito para los cálculos estadísticos que estoy haciendo y están muy ajustadas a mis casos de uso y, por lo tanto, son menos generales pero considerablemente más rápidas que la biblioteca estándar. Estoy usando el lenguaje de programación D, que tiene un viejo optimizador de mierda, y para grandes matrices de flotadores aleatorios, sigo superando la clasificación C ++ STL de GCC en un 10-20%.
dsimcha

Respuestas:

122

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

Un simple ejemplo:

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

Aquí la cadena de dependencia de los argumentos es muy corta. Si se detiene porque tiene una falta 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 obtiene una pérdida de caché u otro bloqueo en un cálculo, todavía hay otras tres cadenas de dependencia que no dependen del bloqueo. Una CPU fuera de servicio puede ejecutarlos.

Nils Pipenbrinck
fuente
2
Gracias. He intentado desenrollar bucles en este estilo en varios otros lugares de la biblioteca donde estoy calculando sumas y demás, 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 sugiere.
dsimcha
2
Buena respuesta y ejemplo instructivo. Aunque no veo cómo las pérdidas de caché podrían afectar el rendimiento para este ejemplo en particular . Vine a explicarme a mí mismo las diferencias de rendimiento entre las dos piezas de código (en mi máquina, la segunda pieza de código es 2-3 veces más rápida) señalando que la primera desactiva cualquier tipo de paralelismo a nivel de instrucción en los carriles de punto flotante. El segundo permitiría que una CPU superescalar ejecute hasta cuatro adiciones de punto flotante al mismo tiempo.
Toby Brull
2
Tenga en cuenta que el resultado no será numéricamente idéntico al ciclo original al calcular una suma de esta manera.
Barabas
La dependencia llevada a cabo en bucle es un ciclo , la suma. Un núcleo OoO funcionará bien. Aquí, desenrollar podría ayudar a SIMD de punto flotante, pero eso no se trata de OoO.
Veedrac
2
@Nils: No mucho; Las CPU convencionales x86 OoO siguen siendo lo suficientemente similares a Core2 / Nehalem / K10. Ponerse al día después de una pérdida de caché era todavía bastante menor, ocultar la latencia de FP seguía siendo el mayor beneficio. En 2010, las CPU que podían hacer 2 cargas por reloj eran aún más raras (solo AMD porque SnB aún no se había lanzado), por lo que varios acumuladores eran definitivamente menos valiosos para el código entero que ahora (por supuesto, este es un código escalar que debería vectorizarse automáticamente , entonces quién sabe si los compiladores convertirán múltiples acumuladores en elementos vectoriales o en múltiples acumuladores vectoriales ...)
Peter Cordes
25

Esos no harían ninguna diferencia porque estás haciendo la misma cantidad de comparaciones. He aquí un mejor ejemplo. En vez de:

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

escribir:

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

Incluso entonces es casi seguro que no importará, pero ahora está haciendo 50 comparaciones en lugar de 200 (imagine que la comparación es más compleja).

Sin embargo, el desenrollado manual de bucles es en gran medida un artefacto de la historia. Es otra de la creciente lista de cosas que un buen compilador hará por usted cuando sea importante. Por ejemplo, la mayoría de la gente no se molesta en escribir x <<= 1o en x += xlugar de x *= 2. Simplemente escriba x *= 2y el compilador lo optimizará para que sea lo mejor.

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

cletus
fuente
1
@Mike Ciertamente desactivar la optimización si es una buena idea cuando estás perplejo, pero vale la pena leer el enlace que publicó Poita_. Los compiladores se están volviendo tremendamente buenos en ese negocio.
dmckee --- ex-moderador gatito
16
@Mike "Soy perfectamente capaz de decidir cuándo o cuándo no hacer esas cosas" ... Lo dudo, a menos que seas sobrehumano.
Mr. Boy
5
@John: No sé por qué dices eso; la gente parece pensar que la optimización es una especie de arte negro que solo los compiladores y los buenos adivinos saben cómo hacerlo. Todo se reduce a instrucciones y ciclos y las razones por las que se gastan. Como he explicado muchas veces en SO, es fácil saber cómo y por qué se gastan. Si tengo un bucle que tiene que usar un porcentaje significativo de tiempo y pasa demasiados ciclos en la sobrecarga del bucle, en comparación con el contenido, puedo verlo y desenrollarlo. Lo mismo para la elevación de código. No hace falta ser un genio.
Mike Dunlavey
3
Estoy seguro de que no es tan difícil, pero todavía dudo que puedas hacerlo tan rápido como lo hace el compilador. ¿Cuál es el problema con que el compilador lo haga por usted de todos modos? Si no le gusta, simplemente desactive las optimizaciones y gaste su tiempo como si fuera 1990.
Mr. Boy
2
La ganancia de rendimiento debida al desenrollado del bucle no tiene nada que ver con las comparaciones que está guardando. Nada en absoluto.
bobbogo
14

Independientemente de la predicción de la rama en el hardware moderno, la mayoría de los compiladores realizan el desenrollado de bucles de todos modos.

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

Encontré la presentación de Felix von Leitner muy esclarecedora sobre el tema. Te recomiendo que lo leas. Resumen: Los compiladores modernos son MUY inteligentes, por lo que las optimizaciones manuales casi nunca son efectivas.

Peter Alexander
fuente
7
Esa es una buena lectura, pero la única parte que pensé que estaba acertada fue cuando habla de mantener la estructura de datos simple. El resto fue preciso, pero se basa en una suposición gigante no declarada: que lo que se está ejecutando tiene que ser. En el ajuste que hago, encuentro que la gente se preocupa por los registros y las pérdidas de caché cuando se dedican enormes cantidades de tiempo a montañas innecesarias de código de abstracción.
Mike Dunlavey
4
"Las optimizaciones manuales casi nunca son efectivas" → Quizás sea cierto si eres completamente nuevo en la tarea. Simplemente no es cierto de otra manera.
Veedrac
En 2019 todavía hice desenrollamientos manuales con ganancias sustanciales sobre los intentos automáticos del compilador ... por lo que no es tan confiable dejar que el compilador lo haga todo. Parece que no se desenrolla con tanta frecuencia. Al menos para c # no puedo hablar en nombre de todos los idiomas.
WDUK
2

Por lo que yo entiendo, los compiladores modernos ya desenrollan los bucles cuando corresponde; un ejemplo es gcc, si se pasan las marcas de optimización, el manual dice que lo hará:

Desenrolle los bucles cuyo número de iteraciones se puede determinar en el momento de la compilación o al ingresar al bucle.

Entonces, en la práctica, es probable que su compilador haga los casos triviales por usted. Por lo tanto, depende de usted asegurarse de que la mayor cantidad posible de sus bucles sea fácil para que el compilador determine cuántas iteraciones se necesitarán.

Rich Bradshaw
fuente
Los compiladores justo a tiempo no suelen desenrollar bucles, las heurísticas son demasiado caras. Los compiladores estáticos pueden dedicarle más tiempo, pero la diferencia entre las dos formas dominantes es importante.
Abel
2

El desenrollado de bucles, ya sea un desenrollado manual o un desenrollado del compilador, a menudo puede ser contraproducente, especialmente con las CPU x86 más recientes (Core 2, Core i7). En pocas palabras: evalúe su código con y sin desenrollado de bucles en las CPU en las que planea implementar este código.

Paul R
fuente
¿Por qué particularmente en CPUs x86 recet?
JohnTortugo
7
@JohnTortugo: Las CPU x86 modernas tienen ciertas optimizaciones para bucles pequeños; consulte, por ejemplo, Loop Stream Detector en las arquitecturas Core y Nehalem; desenrollar un bucle para que ya no sea lo suficientemente pequeño como para caber en el caché LSD anula esta optimización. Véase, por ejemplo, tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R
1

Intentar sin saberlo no es la forma de hacerlo.
¿Toma este tipo un alto porcentaje del tiempo total?

Todo lo que hace el desenrollado de bucle es reducir la sobrecarga del bucle de incrementar / disminuir, comparar la condición de parada y saltar. Si lo que está haciendo en el bucle requiere más ciclos de instrucción que la sobrecarga del bucle en sí, no verá mucha mejora porcentual.

A continuación, se muestra un ejemplo de cómo obtener el máximo rendimiento.

Mike Dunlavey
fuente
1

El desenrollado de bucles puede resultar útil en casos específicos. ¡La única ventaja no es saltarse algunas pruebas!

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

Sin embargo, como se dijo antes, depende mucho del bucle 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)

Kamchatka
fuente
0

El desenrollado del bucle depende completamente del tamaño del problema. Depende por completo de que su algoritmo pueda reducir el tamaño en grupos de trabajo más pequeños. Lo que hiciste arriba no se ve así. No estoy seguro de si se puede desenrollar una simulación de monte carlo.

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

jwendl
fuente
Estaba desenrollando una clasificación rápida que se llama desde el bucle interno de mi simulación, no desde el bucle principal de la simulación.
dsimcha
0

El desenrollado de 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, usa una pequeña cantidad de variables locales, sin abusar de los registros.

La comparación (hasta el final del ciclo) también es un gran inconveniente si la comparación es pesada (es decir, sin testinstrucciones), especialmente si depende de una función externa.

El desenrollado de bucles también ayuda a aumentar la conciencia de la CPU para la predicción de ramas, pero eso ocurre de todos modos.

LiraNuna
fuente