performance assembly x86 cpu-architecture micro-optimization

performance - ¿Se reduce el rendimiento al ejecutar bucles cuyo recuento de uop no es un múltiplo del ancho del procesador?



assembly x86 (2)

Este es un seguimiento de la respuesta original, para analizar el comportamiento de cinco arquitecturas adicionales, en base a los resultados de las pruebas proporcionadas por Andreas Abel :

  • Nehalem
  • Sandy Bridge
  • Ivy Bridge
  • Broadwell
  • Coffee Lake

Echamos un vistazo rápido a los resultados de estas arquitecturas, además de Skylake y Haswell. Solo necesita ser una mirada "rápida" ya que todas las arquitecturas, excepto Nehalem, siguen uno de los patrones existentes discutidos anteriormente.

Primero, el breve caso de nop que ejercita el decodificador heredado (para bucles que no caben en el LSD) y el LSD. Aquí están los ciclos / iteración para este escenario, para las 7 arquitecturas.

Figura 2.1: Todas las arquitecturas de rendimiento denso nop:

Este gráfico está muy ocupado (haga clic para obtener una vista más grande) y un poco difícil de leer ya que los resultados de muchas arquitecturas se encuentran uno encima del otro, pero traté de asegurarme de que un lector dedicado pueda rastrear la línea para cualquier arquitectura.

Primero, discutamos el gran valor atípico: Nehalem. Todas las demás arquitecturas tienen una pendiente que sigue aproximadamente la línea de 4 uops / ciclo, pero Nehalem está a casi exactamente 3 uops por ciclo, por lo que rápidamente queda atrás de todas las demás arquitecturas. Fuera de la región LSD inicial, la línea también es totalmente suave, sin la apariencia de "escalón" que se ve en las otras arquitecturas.

Esto es completamente consistente con que Nehalem tenga un límite de retiro de 3 uops / ciclo. Este es el cuello de botella para los uops fuera del LSD: todos se ejecutan a aproximadamente 3 uops por ciclo, con cuellos de botella en la jubilación. El front-end no es el cuello de botella, por lo que el conteo exacto de UOP y la disposición de decodificación no importan, por lo que el escalón está ausente.

Además de Nehalem, las otras arquitecturas, excepto Broadwell, se dividieron de manera bastante limpia en grupos: tipo Haswell o Skylake. Es decir, todos los de Sandy Bridge, Ivy Bridge y Haswell se comportan como Haswell, para bucles mayores de aproximadamente 15 uops (el comportamiento de Haswell se discute en la otra respuesta). Aunque son microarquitecturas diferentes, se comportan en gran medida de la misma manera, ya que sus capacidades de decodificación heredadas son las mismas. Por debajo de aproximadamente 15 uops, vemos a Haswell como algo más rápido para cualquier conteo de uops que no sea un múltiplo de 4. Quizás tenga un desenrollamiento adicional en el LSD debido a un LSD más grande, o hay otras optimizaciones de "bucle pequeño". Para Sandy Bridge e Ivy Bridge, esto significa que los bucles pequeños definitivamente deberían apuntar a un conteo de UOP que es un múltiplo de 4.

Coffee Lake se comporta de manera similar a Skylake 1 . Esto tiene sentido, ya que la microarquitectura es la misma. Coffee Lake parece mejor que Skylake por debajo de aproximadamente 16 uops, pero esto es solo un efecto del LSD deshabilitado de Coffee Lake por defecto. Skylake se probó con un LSD habilitado, antes de que Intel lo deshabilitara mediante la actualización de microcódigo debido a un problema de seguridad. Coffee Lake se lanzó después de que se conociera este problema, por lo que el LSD se desactivó de inmediato. Entonces, para esta prueba, Coffee Lake está utilizando el DSB (para bucles por debajo de aproximadamente 18 uops, que aún pueden caber en el DSB) o el decodificador heredado (para el resto de los bucles), lo que conduce a mejores resultados para un pequeño recuento de uop bucles donde el LSD impone una sobrecarga (interesante, para bucles más grandes, el LSD y el decodificador heredado imponen exactamente la misma sobrecarga, por razones muy diferentes).

Finalmente, echamos un vistazo a los NOP de 2 bytes, que no son lo suficientemente densos como para evitar el uso del DSB (por lo que este caso refleja más el código típico).

Figura 2.1: rendimiento de nop de 2 bytes:

Nuevamente, el resultado está en la misma línea que el gráfico anterior. Nehalem sigue siendo el cuello de botella atípico a 3 uops por ciclo. Para el rango de hasta aproximadamente 60 uops, todas las arquitecturas que no sean Coffee Lake están utilizando el LSD, y vemos que Sandy Bridge e Ivy Bridge funcionan un poco peor aquí, redondeando al siguiente ciclo y solo logrando el rendimiento máximo de 4 uops / cycle si el número de uops en el bucle es un múltiplo de 4. Por encima de 32 uops, la función "desenrollar" de Haswell y los nuevos uarchs no tiene ningún efecto, por lo que todo está más o menos vinculado.

Sandy Bridge en realidad tiene algunos rangos de UOP (por ejemplo, de 36 a 44 Uops) donde funciona mejor que las arquitecturas más nuevas. Esto parece ocurrir porque el LSD no detecta todos los bucles y en estos rangos los bucles son servidos desde el DSB. Como el DSB es generalmente más rápido, también lo es Sandy Bridge en estos casos.

Lo que dice Intel

En realidad, puede encontrar una sección que trata específicamente este tema en el Manual de optimización de Intel, sección 3.4.2.5, como lo señala Andreas Abel en los comentarios. Allí, Intel dice:

El LSD tiene micro-operaciones que construyen pequeños bucles "infinitos". Las microoperaciones del LSD se asignan en el motor fuera de servicio. El bucle en el LSD termina con una rama tomada al comienzo del bucle. La rama tomada al final del ciclo es siempre la última microoperación asignada en el ciclo. La instrucción al comienzo del ciclo siempre se asigna al siguiente ciclo. Si el rendimiento del código está limitado por el ancho de banda del front-end, las ranuras de asignación no utilizadas generan una burbuja en la asignación y pueden causar una degradación del rendimiento. El ancho de banda de asignación en el nombre de código de microarquitectura Intel Sandy Bridge es de cuatro microoperaciones por ciclo. El rendimiento es mejor, cuando el número de microoperaciones en el LSD da como resultado el menor número de ranuras de asignación no utilizadas. Puede usar el desenrollado de bucle para controlar la cantidad de microoperaciones que hay en el LSD.

Continúan mostrando un ejemplo en el que desenrollar un bucle por un factor de dos no ayuda al rendimiento debido al "redondeo" del LSD, sino al desenrollado por tres trabajos. El ejemplo es muy confuso ya que en realidad mezcla dos efectos, ya que desenrollar más también reduce la sobrecarga del bucle y, por lo tanto, el número de uops por iteración. Un ejemplo más interesante habría sido cuando desenrollar el bucle menos veces condujo a un aumento en el rendimiento debido a los efectos de redondeo de LSD.

Esta sección parece describir con precisión el comportamiento en Sandy Bridge e Ivy Bridge. Los resultados anteriores muestran que ambas arquitecturas funcionan como se describe y pierde 1, 2 o 3 ranuras de ejecución de bucles para bucles con 4N + 3, 4N + 2 o 4N + 1 uops respectivamente.

Sin embargo, no se ha actualizado con el nuevo rendimiento para Haswell y más tarde. Como se describe en la otra respuesta, el rendimiento ha mejorado desde el modelo simple descrito anteriormente y el comportamiento es más complejo.

1 Hay un valor atípico extraño a las 16 uops en el que Coffee Lake se desempeña peor que todas las demás arquitecturas, incluso Nehalem (una regresión de aproximadamente el 50%), pero ¿tal vez este ruido de medición?

Me pregunto cómo funcionan los bucles de varios tamaños en los procesadores x86 recientes, en función del número de uops.

Aquí hay una cita de Peter Cordes que planteó el tema de los recuentos no múltiples de 4 en otra pregunta :

También descubrí que el ancho de banda de uop fuera del búfer de bucle no es un constante 4 por ciclo, si el bucle no es un múltiplo de 4 uops. (es decir, es abc, abc, ...; no abca, bcab, ...). El documento de microarchivo de Agner Fog desafortunadamente no estaba claro sobre esta limitación del búfer de bucle.

El problema es si los bucles deben ser un múltiplo de N uops para ejecutar al máximo rendimiento de uop, donde N es el ancho del procesador. (es decir, 4 para procesadores Intel recientes). Hay muchos factores complicados cuando hablamos de "ancho" y cuenta Uops, pero sobre todo quiero ignorarlos. En particular, suponga que no hay micro o macro fusión.

Peter da el siguiente ejemplo de un bucle con 7 uops en su cuerpo:

Un bucle de 7 uop emitirá grupos de 4 | 3 | 4 | 3 | ... No he probado bucles más grandes (que no caben en el búfer de bucle) para ver si es posible para la primera instrucción de la próxima iteración para emitir en el mismo grupo que la rama tomada, pero supongo que no.

En términos más generales, la afirmación es que cada iteración de un bucle con x uops en su cuerpo tomará al menos iteraciones ceil ceil(x / 4) , en lugar de simplemente x / 4 .

¿Es esto cierto para algunos o todos los procesadores compatibles con x86 recientes?


Investigué un poco con el perf Linux para ayudar a responder esto en mi caja Skylake i7-6700HQ , y los resultados de Haswell han sido amablemente proporcionados por otro usuario. El análisis a continuación se aplica a Skylake, pero es seguido por una comparación versus Haswell.

Otras arquitecturas pueden variar 0 , y para ayudar a resolverlo, agradezco los resultados adicionales. La fuente está disponible ).

Esta pregunta trata principalmente con el front-end, ya que en arquitecturas recientes es el front-end el que impone el límite estricto de cuatro uops de dominio fusionado por ciclo.

Resumen de reglas para el rendimiento del bucle

Primero, resumiré los resultados en términos de algunas "reglas de rendimiento" a tener en cuenta al tratar con pequeños bucles. También hay muchas otras reglas de rendimiento: estas son complementarias a ellas (es decir, probablemente no rompa otra regla solo para satisfacerlas). Estas reglas se aplican más directamente a Haswell y arquitecturas posteriores; consulte la otra respuesta para obtener una descripción general de las diferencias en arquitecturas anteriores.

Primero, cuente la cantidad de uops fusionados con macro en su bucle. Puede usar las tablas de instrucciones de Agner para buscar esto directamente para cada instrucción, excepto que una rama de ALU y una rama inmediatamente siguiente generalmente se fusionarán en una sola uop. Luego basado en este recuento:

  • Si el recuento es un múltiplo de 4, está bien: estos bucles se ejecutan de manera óptima.
  • Si el recuento es par y menor que 32, está bien, excepto si es 10, en cuyo caso debe desenrollar a otro número par si puede.
  • Para números impares, debe intentar desenrollar a un número par menor que 32 o un múltiplo de 4, si puede.
  • Para bucles de más de 32 uops pero menos de 64, es posible que desee desenrollar si aún no es un múltiplo de 4: con más de 64 uops obtendrá un rendimiento eficiente en cualquier valor en Sklyake y casi todos los valores en Haswell ( con algunas desviaciones, posiblemente relacionadas con la alineación). Las ineficiencias de estos bucles siguen siendo relativamente pequeñas: los valores que se deben evitar son los recuentos de 4N + 1 , seguidos de los recuentos de 4N + 2 .

Resumen de resultados

Para el código servido fuera de la caché de uop, no hay efectos aparentes de múltiplo de 4. Los bucles de cualquier número de uops se pueden ejecutar con un rendimiento de 4 uops de dominio fusionado por ciclo.

Para el código procesado por los decodificadores heredados, lo contrario es cierto: el tiempo de ejecución del bucle se limita al número integral de ciclos, y por lo tanto los bucles que no son múltiplos de 4 uops no pueden alcanzar 4 uops / ciclo, ya que desperdician algunos espacios de emisión / ejecución .

Para el código emitido por el detector de flujo de bucle (LSD), la situación es una mezcla de los dos y se explica con más detalle a continuación. En general, los bucles de menos de 32 uops y con un número par de uops se ejecutan de manera óptima, mientras que los bucles de tamaños impares no lo hacen, y los bucles más grandes requieren un conteo múltiple de 4 uop para ejecutarse de manera óptima.

Lo que dice Intel

Intel en realidad tiene una nota sobre esto en su manual de optimización, detalles en la otra respuesta.

Detalles

Como cualquier persona conoce bien las arquitecturas x86-64 recientes, en cualquier momento la parte de captación y decodificación del front-end puede estar funcionando en varios modos diferentes, dependiendo del tamaño del código y otros factores. Como resultado, estos modos diferentes tienen comportamientos diferentes con respecto al tamaño del bucle. Los cubriré por separado a continuación.

Decodificador heredado

El decodificador heredado 1 es el decodificador completo de código de máquina a uops que se usa 2 cuando el código no cabe en los mecanismos de almacenamiento en caché de uop (LSD o DSB). La razón principal por la que esto ocurriría es si el conjunto de trabajo del código es más grande que la caché uop (aproximadamente ~ 1500 uops en el caso ideal, menos en la práctica). Sin embargo, para esta prueba, aprovecharemos el hecho de que el decodificador heredado también se usará si un fragmento alineado de 32 bytes contiene más de 18 instrucciones 3 .

Para probar el comportamiento del decodificador heredado, usamos un bucle que se ve así:

short_nop: mov rax, 100_000_000 ALIGN 32 .top: dec rax nop ... jnz .top ret

Básicamente, un bucle trivial que cuenta hasta que rax sea ​​cero. Todas las instrucciones son un solo uop 4 y el número de instrucciones de nop varía (en la ubicación que se muestra como ... ) para probar diferentes tamaños de bucles (por lo que un bucle de 4 uop tendrá 2 nop s, más las dos instrucciones de control de bucle ) No hay macro fusión ya que siempre separamos el dec y jnz con al menos un nop , y tampoco hay micro fusión. Finalmente, no hay acceso a la memoria en (fuera del acceso implícito de icache).

Tenga en cuenta que este bucle es muy denso , aproximadamente 1 byte por instrucción (ya que las instrucciones nop son de 1 byte cada una), por lo que activaremos las> 18 instrucciones en una condición de fragmento de 32B tan pronto como lleguemos a 19 instrucciones en el bucle. Según el examen de los contadores de rendimiento de rendimiento lsd.uops e idq.mite_uops eso es exactamente lo que vemos: esencialmente, el 100% de las instrucciones salen del LSD 5 hasta el bucle de 18 uop, incluido el bucle de 18 uops o más, el 100% provienen del decodificador heredado.

En cualquier caso, aquí están los ciclos / iteración para todos los tamaños de bucles de 3 a 99 uops 6 :

Los puntos azules son los bucles que se ajustan al LSD y muestran un comportamiento algo complejo. Veremos esto más tarde.

Los puntos rojos (a partir de 19 uops / iteración) son manejados por el decodificador heredado y muestran un patrón muy predecible:

  • Todos los bucles con N uops toman exactamente iteraciones de ceiling(N/4)

Entonces, al menos para el decodificador heredado, la observación de Peter se mantiene exactamente en Skylake: los bucles con un múltiplo de 4 uops pueden ejecutarse a un IPC de 4, pero cualquier otro número de uops desperdiciará 1, 2 o 3 espacios de ejecución (para bucles con 4N+3 , 4N+2 , 4N+1 instrucciones, respectivamente).

No me queda claro por qué sucede esto. Aunque puede parecer obvio si considera que la decodificación ocurre en fragmentos contiguos de 16B, por lo que a una velocidad de decodificación de 4 uops / ciclos, no un múltiplo de 4 siempre tendría algunas ranuras finales (desperdiciadas) en el ciclo en el que se encuentra la instrucción jnz . Sin embargo, la unidad real de captación y decodificación está compuesta de fases de predecodificación y decodificación, con una cola intermedia. La fase de precodificación en realidad tiene un rendimiento de 6 instrucciones, pero solo decodifica al final del límite de 16 bytes en cada ciclo. Esto parece implicar que la burbuja que se produce al final del bucle podría ser absorbida por el precodificador -> cola de decodificación ya que el precodificador tiene un rendimiento promedio superior a 4.

Por lo tanto, no puedo explicar completamente esto en base a mi comprensión de cómo funciona el predecodificador. Puede ser que haya alguna limitación adicional en la decodificación o predescodificación que evite los recuentos de ciclos no integrales. Por ejemplo, quizás los decodificadores heredados no pueden decodificar instrucciones en ambos lados de un salto, incluso si las instrucciones posteriores al salto están disponibles en la cola predefinida. Quizás esté relacionado con la necesidad de handle macro fusión.

La prueba anterior muestra el comportamiento donde la parte superior del bucle está alineada en un límite de 32 bytes. A continuación se muestra el mismo gráfico, pero con una serie adicional que muestra el efecto cuando la parte superior del bucle se mueve 2 bytes hacia arriba (es decir, ahora está desalineado en un límite de 32N + 30):

La mayoría de los tamaños de bucle ahora sufren una penalización de 1 o 2 ciclos. El caso de penalización de 1 tiene sentido cuando considera decodificar los límites de 16B y la decodificación de 4 instrucciones por ciclo, y los casos de penalización de 2 ciclos se producen para bucles donde, por alguna razón, el DSB se usa para 1 instrucción en el bucle (probablemente la instrucción dec que aparece en su propio fragmento de 32 bytes), y se incurre en algunas penalizaciones de cambio DSB <-> MITE.

En algunos casos, la desalineación no duele cuando termina alineando mejor el final del ciclo. Probé la desalineación y persiste de la misma manera hasta 200 bucles uop. Si toma la descripción de los predecodificadores al pie de la letra, parecería que, como se indicó anteriormente, deberían poder ocultar una burbuja de búsqueda por desalineación, pero no sucede (tal vez la cola no sea lo suficientemente grande).

DSB (caché Uop)

El caché uop (a Intel le gusta llamarlo DSB) puede almacenar en caché la mayoría de los bucles de una cantidad moderada de instrucciones. En un programa típico, esperaría que la mayoría de sus instrucciones se sirvan de este caché 7 .

Podemos repetir la prueba anterior, pero ahora sirve uops fuera de la caché de uop. Esto es una simple cuestión de aumentar el tamaño de nuestros nops a 2 bytes, por lo que ya no alcanzamos el límite de 18 instrucciones. Usamos el 2-byte nop xchg ax, ax en nuestro bucle:

long_nop_test: mov rax, iters ALIGN 32 .top: dec eax xchg ax, ax ; this is a 2-byte nop ... xchg ax, ax jnz .top ret

Aquí, los resultados son muy sencillos. Para todos los tamaños de bucles probados entregados desde el DSB, el número de ciclos requeridos fue N/4 , es decir, los bucles ejecutados al máximo rendimiento teórico, incluso si no tenían un múltiplo de 4 uops. Por lo tanto, en general, en Skylake, los bucles de tamaño moderado que se envían desde el DSB no deberían preocuparse por garantizar que el conteo de UOP cumpla con un múltiplo particular.

Aquí hay un gráfico de 1,000 bucles uop. Si entrecierra los ojos, puede ver el comportamiento subóptimo antes de 64 uops (cuando el bucle está en el LSD). Después de eso, es un tiro directo, 4 IPC todo el camino a 1,000 uops (con un blip alrededor de 900 que probablemente se debió a cargar en mi caja):

A continuación, observamos el rendimiento de los bucles que son lo suficientemente pequeños como para caber en la caché de uop.

LSD (detector de vapor de bucle)

Nota importante: Intel aparentemente ha desactivado los chips LSD en Skylake (SKL150 erratum) y Kaby Lake (KBL095, KBW095 erratum) a través de una actualización de microcódigo y en Skylake-X fuera de la caja, debido a un error relacionado con la interacción entre hyperthreading y El LSD. Para esos chips, el gráfico a continuación probablemente no tendrá la región interesante hasta 64 uops; más bien, se verá igual que la región después de 64 uops.

El detector de flujo de bucle puede almacenar en caché bucles pequeños de hasta 64 uops (en Skylake). En la documentación reciente de Intel, se posiciona más como un mecanismo de ahorro de energía que como una característica de rendimiento, aunque ciertamente no hay inconvenientes de rendimiento mencionados al usar el LSD.

Al ejecutar esto para los tamaños de bucle que deberían caber en el LSD, obtenemos los siguientes ciclos / comportamiento de iteración:

La línea roja aquí es el% de uops que se entregan desde el LSD. Se alinea al 100% para todos los tamaños de bucles de 5 a 56 uops.

Para los 3 y 4 bucles uop, tenemos el comportamiento inusual de que el 16% y el 25% de los uops, respectivamente, se entregan desde el decodificador heredado. ¿Eh? Afortunadamente, no parece afectar el rendimiento del bucle ya que ambos casos alcanzan el rendimiento máximo de 1 bucle / ciclo, a pesar del hecho de que uno podría esperar algunas penalizaciones de transición MITE <-> LSD.

Entre los tamaños de bucle de 57 y 62 uops, el número de uops entregados desde LSD exhibe un comportamiento extraño: aproximadamente el 70% de los uops se entregan desde el LSD y el resto desde el DSB. Skylake nominalmente tiene un LSD de 64 uop, por lo que esta es una especie de transición justo antes de que se exceda el tamaño del LSD, tal vez hay algún tipo de alineación interna dentro del IDQ (en el que se implementa el LSD) que causa solo golpes parciales en el LSD en esta fase. Esta fase es corta y, en cuanto al rendimiento, parece ser principalmente una combinación lineal del rendimiento completo en LSD que le precede, y el rendimiento totalmente en DSB que le sigue.

Veamos el cuerpo principal de resultados entre 5 y 56 uops. Vemos tres regiones distintas:

Bucles de 3 a 10 uops: aquí, el comportamiento es complejo. Es la única región donde vemos recuentos de ciclos que no pueden explicarse por el comportamiento estático en una iteración de bucle único 8 . El rango es lo suficientemente corto que es difícil decir si hay un patrón. Los bucles de 4, 6 y 8 uops se ejecutan de manera óptima, en N/4 ciclos (ese es el mismo patrón que la siguiente región).

Un bucle de 10 uops, por otro lado, se ejecuta en 2.66 ciclos por iteración, lo que lo convierte en el único tamaño de bucle uniforme que no se ejecuta de manera óptima hasta llegar a tamaños de bucle de 34 uops o más (aparte del valor atípico en 26) . Eso corresponde a algo así como una tasa de ejecución repetida uop / ciclo de 4, 4, 4, 3 . Para un ciclo de 5 uops, obtienes 1.33 ciclos por iteración, muy cerca pero no es lo mismo que el ideal de 1.25. Eso corresponde a una tasa de ejecución de 4, 4, 4, 4, 3 .

Estos resultados son difíciles de explicar. Los resultados son repetibles de una ejecución a otra, y robustos a cambios tales como cambiar el nop por una instrucción que realmente hace algo como mov ecx, 123 . Puede tener algo que ver con el límite de 1 rama tomada cada 2 ciclos, que se aplica a todos los bucles, excepto a los que son "muy pequeños". Puede ser que los uops ocasionalmente se alineen de tal manera que esta limitación entre en acción, lo que lleva a un ciclo adicional. Una vez que llegue a 12 uops o más, esto nunca ocurre ya que siempre está tomando al menos tres ciclos por iteración.

Bucles de 11 a 32 uops: vemos un patrón de escalones, pero con un período de dos. Básicamente, todos los bucles con un número par de uops funcionan de manera óptima, es decir, toman exactamente N/4 ciclos. Los bucles con un número impar de uops desperdician una "ranura de emisión" y toman el mismo número de ciclos que un bucle con uno más (es decir, un bucle de 17 uop toma los mismos 4.5 ciclos que un bucle de 18 uop). Entonces, aquí tenemos un comportamiento mejor que el ceiling(N/4) para muchos recuentos de UOP, y tenemos la primera evidencia de que Skylake al menos puede ejecutar bucles en un número no integral de ciclos.

Los únicos valores atípicos son N = 25 y N = 26, que toman aproximadamente un 1,5% más de lo esperado. Es pequeño pero reproducible y robusto para mover la función en el archivo. Eso es demasiado pequeño para ser explicado por un efecto por iteración, a menos que tenga un período gigante, por lo que probablemente sea otra cosa.

El comportamiento general aquí es exactamente consistente (fuera de la anomalía 25/26) con el hardware que desenrolla el bucle por un factor de 2.

Bucles de 33 a ~ 64 uops: vemos un patrón de escalón de nuevo, pero con un período de 4 y un rendimiento promedio peor que el caso de hasta 32 uop. El comportamiento es exactamente el ceiling(N/4) , es decir, el mismo que el caso del decodificador heredado. Entonces, para bucles de 32 a 64 uops, el LSD no proporciona ningún beneficio aparente sobre los decodificadores heredados, en términos de rendimiento de front-end para esta limitación particular . Por supuesto, hay muchas otras formas en que el LSD es mejor: evita muchos de los posibles cuellos de botella de decodificación que se producen para instrucciones más complejas o más largas, y ahorra energía, etc.

Todo esto es bastante sorprendente, porque significa que los bucles entregados desde la caché de uop generalmente funcionan mejor en el extremo frontal que los bucles entregados desde el LSD, a pesar de que el LSD generalmente se posiciona como una fuente de uops estrictamente mejor que el DSB (por ejemplo, como parte de los consejos para tratar de mantener los bucles lo suficientemente pequeños como para caber en el LSD).

Aquí hay otra forma de ver los mismos datos, en términos de la pérdida de eficiencia para un conteo de UOP dado, en comparación con el rendimiento máximo teórico de 4 Uops por ciclo. Un golpe de eficiencia del 10% significa que solo tiene el 90% del rendimiento que calcularía a partir de la fórmula N/4 simple.

El comportamiento general aquí es consistente con el hardware que no se desenrolla, lo que tiene sentido ya que un bucle de más de 32 uops no se puede desenrollar en absoluto en un búfer de 64 uops.

Las tres regiones discutidas anteriormente están coloreadas de manera diferente, y al menos los efectos competitivos son visibles:

  1. Todo lo demás es igual, cuanto mayor sea el número de uops involucrados, menor será el golpe de eficiencia. El éxito es un costo fijo solo una vez por iteración, por lo que los bucles más grandes pagan un costo relativo menor.

  2. Hay un gran salto en la ineficiencia cuando cruza hacia la región de más de 33 uop: tanto el tamaño de la pérdida de rendimiento aumenta como el número de recuentos de uop afectados se duplica.

  3. La primera región es algo caótica, y 7 uops es el peor recuento total de uops.

Alineación

El análisis DSB y LSD anterior es para entradas de bucle alineadas a un límite de 32 bytes, pero el caso no alineado no parece sufrir en ninguno de los casos: no hay una diferencia material del caso alineado (aparte de quizás alguna pequeña variación por menos de 10 uops que no investigé más).

Aquí están los resultados no alineados para 32N-2 y 32N+2 (es decir, los 2 bytes superiores del bucle antes y después del límite de 32B):

La línea N/4 ideal también se muestra como referencia.

Haswell

A continuación, eche un vistazo a la microarquitectura anterior: Haswell. Los números aquí han sido proporcionados gentilmente por el usuario Iwillnotexist Idonotexist .

Tubería de decodificación LSD + Legacy

Primero, los resultados de la prueba de "código denso" que prueba el LSD (para recuentos de UOP pequeños) y la tubería heredada (para recuentos de UOP más grandes, ya que el bucle "se rompe" del DSB debido a la densidad de la instrucción.

Inmediatamente vemos una diferencia ya en términos de cuándo cada arquitectura ofrece uops desde el LSD para un bucle denso. A continuación comparamos Skylake y Haswell para bucles cortos de código denso (1 byte por instrucción).

Como se describió anteriormente, el bucle Skylake deja de ser entregado desde el LSD a exactamente 19 uops, como se esperaba de la región de límite de código de 18 uop por 32 bytes. Haswell, por otro lado, parece dejar de entregar de manera confiable desde el LSD para los bucles de 16 uop y 17 uop también. No tengo ninguna explicación para esto. También hay una diferencia en el caso de 3 uop: curiosamente, ambos procesadores solo entregan algunos de sus uops fuera del LSD en los casos de 3 y 4 uop, pero la cantidad exacta es la misma para 4 uops, y diferente de 3.

Sin embargo, nos preocupamos principalmente por el rendimiento real, ¿verdad? Así que echemos un vistazo a los ciclos / iteración para el caso de código denso alineado de 32 bytes:

Estos son los mismos datos que se muestran arriba para Skylake (la serie desalineada ha sido eliminada), con Haswell tramado al lado. Inmediatamente observa que el patrón es similar para Haswell, pero no el mismo. Como arriba, hay dos regiones aquí:

Decodificación heredada

Los bucles más grandes que ~ 16-18 uops (la incertidumbre se describe anteriormente) se entregan desde los decodificadores heredados. El patrón para Haswell es algo diferente de Skylake.

Para el rango de 19-30 uops son idénticos, pero después de eso Haswell rompe el patrón. Skylake tomó ciclos de ceil(N/4) para los bucles entregados desde los decodificadores heredados. Haswell, por otro lado, parece tomar algo como ceil((N+1)/4) + ceil((N+2)/12) - ceil((N+1)/12) . OK, eso es desordenado (forma más corta, ¿alguien?), Pero básicamente significa que mientras Skylake ejecuta bucles con 4 * N ciclos de manera óptima (es decir, a 4 uops / ciclo), tales bucles son (localmente) el recuento menos óptimo (al menos localmente): se necesita un ciclo más para ejecutar dichos bucles que Skylake. Entonces, en realidad, es mejor con bucles de 4N-1 uops en Haswell, excepto que el 25% de dichos bucles que también tienen la forma 16-1N (31, 47, 63, etc.) toman un ciclo adicional. Está comenzando a sonar como un cálculo de año bisiesto, pero el patrón probablemente se entienda mejor visualmente arriba.

No creo que este patrón sea intrínseco al envío de UOP en Haswell, por lo que no deberíamos leer demasiado. Parece explicarse por

0000000000455a80 <short_nop_aligned35.top>: 16B cycle 1 1 455a80: ff c8 dec eax 1 1 455a82: 90 nop 1 1 455a83: 90 nop 1 1 455a84: 90 nop 1 2 455a85: 90 nop 1 2 455a86: 90 nop 1 2 455a87: 90 nop 1 2 455a88: 90 nop 1 3 455a89: 90 nop 1 3 455a8a: 90 nop 1 3 455a8b: 90 nop 1 3 455a8c: 90 nop 1 4 455a8d: 90 nop 1 4 455a8e: 90 nop 1 4 455a8f: 90 nop 2 5 455a90: 90 nop 2 5 455a91: 90 nop 2 5 455a92: 90 nop 2 5 455a93: 90 nop 2 6 455a94: 90 nop 2 6 455a95: 90 nop 2 6 455a96: 90 nop 2 6 455a97: 90 nop 2 7 455a98: 90 nop 2 7 455a99: 90 nop 2 7 455a9a: 90 nop 2 7 455a9b: 90 nop 2 8 455a9c: 90 nop 2 8 455a9d: 90 nop 2 8 455a9e: 90 nop 2 8 455a9f: 90 nop 3 9 455aa0: 90 nop 3 9 455aa1: 90 nop 3 9 455aa2: 90 nop 3 9 455aa3: 75 db jne 455a80 <short_nop_aligned35.top>

Aquí he notado el fragmento de decodificación 16B (1-3) en el que aparece cada instrucción y el ciclo en el que se decodificará. La regla es básicamente que se decodifiquen hasta las siguientes 4 instrucciones, siempre que se encuentren en el bloque 16B actual. De lo contrario, tienen que esperar hasta el próximo ciclo. Para N = 35, vemos que hay una pérdida de 1 ranura de decodificación en el ciclo 4 (solo quedan 3 instrucciones en el fragmento 16B), pero que de lo contrario el bucle se alinea muy bien con los límites de 16B e incluso el último ciclo ( 9) puede decodificar 4 instrucciones.

Aquí hay una mirada truncada a N = 36, que es idéntica excepto por el final del ciclo:

0000000000455b20 <short_nop_aligned36.top>: 16B cycle 1 1 455a80: ff c8 dec eax 1 1 455b20: ff c8 dec eax 1 1 455b22: 90 nop ... [29 lines omitted] ... 2 8 455b3f: 90 nop 3 9 455b40: 90 nop 3 9 455b41: 90 nop 3 9 455b42: 90 nop 3 9 455b43: 90 nop 3 10 455b44: 75 da jne 455b20 <short_nop_aligned36.top>

Ahora hay 5 instrucciones para decodificar en el tercer y último trozo de 16B, por lo que se necesita un ciclo adicional. Básicamente 35 instrucciones, porque este patrón particular de instrucciones se alinea mejor con los límites de 16B bits y ahorra un ciclo al decodificar. ¡Esto no significa que N = 35 sea mejor que N = 36 en general! Las diferentes instrucciones tendrán diferentes números de bytes y se alinearán de manera diferente. Un problema de alineación similar explica también el ciclo adicional que se requiere cada 16 bytes:

16B cycle ... 2 7 45581b: 90 nop 2 8 45581c: 90 nop 2 8 45581d: 90 nop 2 8 45581e: 90 nop 3 8 45581f: 75 df jne 455800 <short_nop_aligned31.top>

Aquí el jne final se ha deslizado hacia el siguiente trozo de 16B (si una instrucción abarca un límite de 16B, está efectivamente en el último trozo), causando una pérdida de ciclo adicional. Esto ocurre solo cada 16 bytes.

Entonces, los resultados del decodificador heredado de Haswell se explican perfectamente por un decodificador heredado que se comporta como se describe, por ejemplo, en el documento de microarquitectura de Agner Fog. De hecho, también parece explicar los resultados de Skylake si supone que Skylake puede decodificar 5 instrucciones por ciclo (entregando hasta 5 uops) 9 . Suponiendo que pueda, el rendimiento de decodificación heredada asintótica en este código para Skylake todavía es de 4 uops, ya que un bloque de 16 nops decodifica 5-5-5-1, frente a 4-4-4-4 en Haswell, por lo que solo obtienes beneficios en los bordes: en el caso N = 36 anterior, por ejemplo, Skylake puede decodificar las 5 instrucciones restantes, en comparación con 4-1 para Haswell, ahorrando un ciclo.

El resultado es que parece ser que el comportamiento del decodificador heredado se puede entender de una manera bastante directa, y el principal consejo de optimización es continuar masajeando el código para que caiga "inteligentemente" en los bloques alineados 16B (tal vez eso es NP- duro como el embalaje del contenedor?).

DSB (y LSD nuevamente)

A continuación, echemos un vistazo al escenario en el que el código se sirve desde el LSD o el DSB, mediante el uso de la prueba "long nop" que evita romper el límite de 18-uop por 32B, y así permanece en el DSB.

Haswell vs Skylake:

Tenga en cuenta el comportamiento del LSD: aquí Haswell deja de servir fuera del LSD a exactamente 57 uops, lo que es completamente consistente con el tamaño publicado del LSD de 57 uops. No hay un extraño "período de transición" como lo vemos en Skylake. Haswell también tiene el comportamiento extraño para 3 y 4 uops donde solo ~ 0% y ~ 40% de los uops, respectivamente, provienen del LSD.

En cuanto al rendimiento, Haswell normalmente está en línea con Skylake con algunas desviaciones, por ejemplo, alrededor de 65, 77 y 97 uops donde se redondea al siguiente ciclo, mientras que Skylake siempre puede sostener 4 uops / ciclo incluso cuando eso sea un resultado en un número no entero de ciclos. La ligera desviación de lo esperado a los 25 y 26 uops ha desaparecido. Quizás la tasa de entrega de 6 uop de Skylake lo ayuda a evitar problemas de alineación de uop-cache que Haswell sufre con su tasa de entrega de 4 uop.

Otras arquitecturas

El usuario Andreas Abel proporcionó amablemente los resultados de las siguientes arquitecturas adicionales, pero tendremos que usar otra respuesta para un análisis más detallado ya que estamos en el límite de caracteres aquí.

Se necesita ayuda

Aunque los resultados para muchas plataformas han sido amablemente ofrecidos por la comunidad, todavía estoy interesado en resultados en chips más antiguos que Nehalem y más nuevos que Coffee Lake (en particular, Cannon Lake, que es una nueva zona). El código para generar estos resultados es público . Además, los resultados anteriores también están disponibles en formato .ods en GitHub.

0 En particular, el rendimiento máximo del decodificador heredado aparentemente aumentó de 4 a 5 uops en Skylake, y el rendimiento máximo para la caché de uop aumentó de 4 a 6. Ambos podrían afectar los resultados descritos aquí.

1 A Intel realmente le gusta llamar al decodificador heredado MITE (Motor de traducción de microinstrucciones), tal vez porque es un paso en falso etiquetar realmente cualquier parte de su arquitectura con la connotación heredada .

2 Técnicamente, hay otra fuente de uops aún más lenta: el MS (motor de secuenciación de microcódigo), que se usa para implementar cualquier instrucción con más de 4 uops, pero ignoramos esto aquí ya que ninguno de nuestros bucles contiene instrucciones microcodificadas.

3 Esto funciona porque cualquier fragmento alineado de 32 bytes puede usar como máximo 3 vías en su ranura de caché uop, y cada ranura tiene capacidad para 6 uops. Entonces, si usa más de 3 * 6 = 18 uops en un fragmento de 32B, el código no se puede almacenar en la caché de uop. Probablemente sea raro encontrar esta condición en la práctica, ya que el código debe ser muy denso (menos de 2 bytes por instrucción) para activarlo.

4 Las instrucciones nop decodifican en una unidad, pero no se eliminan antes de la ejecución (es decir, no usan un puerto de ejecución), pero aún así ocupan espacio en la parte frontal y, por lo tanto, cuentan contra los diversos límites que estamos interesado en.

5 El LSD es el detector de flujo de bucle , que almacena pequeños bucles de hasta 64 uops (Skylake) directamente en el IDQ. En arquitecturas anteriores, puede contener 28 uops (ambos núcleos lógicos activos) o 56 uops (un núcleo lógico activo).

6 No podemos ajustar fácilmente un bucle de 2 uop en este patrón, ya que eso significaría cero instrucciones nop , lo que significa que las instrucciones dec y jnz se fusionarían con un cambio correspondiente en el recuento de uop. Solo toma mi palabra de que todos los bucles con 4 o menos uops se ejecutan mejor en 1 ciclo / iteración.

7 Por diversión, acabo de ejecutar la perf stat en una corta ejecución de Firefox donde abrí una pestaña e hice clic en algunas preguntas de desbordamiento de pila. Para las instrucciones entregadas, obtuve 46% de DSB, 50% de decodificador heredado y 4% de LSD. Esto muestra que, al menos para un código grande y ramificado como un navegador, el DSB todavía no puede capturar la gran mayoría del código (por suerte, los decodificadores heredados no son tan malos).

8 Con esto quiero decir que todos los otros recuentos de ciclos pueden explicarse simplemente tomando un costo de bucle integral "efectivo" en uops (que podría ser mayor que el tamaño real es uops) y dividiendo por 4. Para estos bucles muy cortos , esto no funciona: no puede llegar a 1.333 ciclos por iteración dividiendo cualquier número entero por 4. Dicho de otra manera, en todas las demás regiones los costos tienen la forma N / 4 para algún número entero N.

9 De hecho, sabemos que Skylake puede entregar 5 uops por ciclo desde el decodificador heredado, pero no sabemos si esos 5 uops pueden provenir de 5 instrucciones diferentes, o solo 4 o menos. Es decir, esperamos que Skylake pueda decodificar en el patrón 2-1-1-1 , pero no estoy seguro de si puede decodificar en el patrón 1-1-1-1-1 . Los resultados anteriores dan alguna evidencia de que efectivamente puede decodificar 1-1-1-1-1 .