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

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 problema de conteos que no son múltiplos de 4 en otra pregunta :

También encontré que el ancho de banda uop fuera del búfer de bucle no es una constante de 4 por ciclo, si el bucle no es un múltiplo de 4 uops. (es decir, es abc, abc, …; no abca, bcab, …). El doctor de fracasado de Agner Fog desafortunadamente no tenía claro esta limitación del búfer de bucle.

El problema es si los bucles deben ser un múltiplo de N uops para ejecutarse 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 se habla de “ancho” y cuenta uops, pero principalmente quiero ignorarlos. En particular, suponga que no hay micro fusión o macro fusión.

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

Un bucle 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 siguiente iteración para emitir en el mismo grupo que la ramificación 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 ceil(x / 4) iteraciones, en lugar de simplemente x / 4 .

¿Esto es cierto para algunos o todos los procesadores recientes compatibles con x86?

Hice algunas investigaciones con Linux perf para ayudar a responder esto en mi caja Skylake i7-6700HQ , y los resultados de Haswell han sido amablemente proporcionados por otro usuario. El siguiente análisis se aplica a Skylake, pero va seguido de una comparación con Haswell.

Otras architectures pueden variar 0 : ¡necesitamos más pruebas de ayuda para confirmar! En ese sentido, recibo resultados adicionales de cualquier persona con otras architectures de CPU (la fuente está disponible ). Puede agregarlo a esta respuesta o crear su respuesta con los hallazgos adicionales.

Esta pregunta se refiere principalmente a la interfaz, ya que en las architectures recientes es la interfaz la que impone el límite estricto de cuatro uops de dominio fusionado por ciclo.

Resumen de reglas para el rendimiento de bucle

En primer lugar, resumiré los resultados en términos de unas pocas “reglas de rendimiento” para tener en cuenta cuando se trate de pequeños bucles. También hay muchas otras reglas de rendimiento, que son complementarias a ellas (es decir, probablemente no rompa otra regla para satisfacerlas).

Primero, cuente la cantidad de uops macro fusionados en su ciclo. Puede usar las tablas de instrucciones de Agner para buscar esto directamente para cada instrucción, excepto que un ALUP uop e inmediatamente seguir a una twig generalmente se fusionarán en un único uop. Luego basado en este conteo:

  • Si el recuento es un múltiplo de 4, está bien: estos bucles se ejecutan de manera óptima.
  • Si el recuento es par y menos de 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 inferior a 32 o un múltiplo de 4, si puede.
  • Para bucles de más de 32 uops pero menos de 64, puede desear desenrollar si no es un múltiplo de 4: con más de 64 uops obtendrá un rendimiento eficiente con cualquier valor en Sklyake y casi todos los valores en Haswell ( con algunas desviaciones, posiblemente relacionadas con la alineación). Las ineficiencias para estos bucles son todavía relativamente pequeñas: los valores a evitar son 4N + 1 conteos, seguidos de 4N + 2 conteos.

Resumen de resultados

Para el código servido fuera de la memoria caché de uop, no hay aparentes múltiples de 4 efectos. 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, ocurre lo contrario: el tiempo de ejecución del bucle está limitado 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 algunas ranuras de emisión / ejecución .

Para el código emitido desde el detector de stream de bucle (LSD), la situación es una mezcla de las dos situaciones 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ño impar no lo hacen, mientras que los bucles más grandes requieren un conteo de uop múltiple de 4 para ejecutarse de manera óptima. Detalles abajo.

Detalles

Como sabe cualquier arquitecto reciente x86-64, en cualquier punto la parte de búsqueda y deencoding de la interfaz 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 lazo. Los cubriré por separado.

Decodificador heredado

El decodificador heredado 1 es el decodificador completo de código máquina a uops que se utiliza 2 cuando el código no encaja en los mecanismos de almacenamiento en caché uop (LSD o DSB). La razón principal por la que esto ocurriría es si el conjunto de códigos de trabajo es más grande que el 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, utilizamos 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 hacia abajo hasta que rax es cero. Todas las instrucciones son un solo uop 4 y el número de instrucciones de nop se varía (en la ubicación mostrada 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 dec y jnz con al menos un nop , y tampoco jnz . Finalmente, no hay acceso a memoria en (fuera del acceso icache implícito).

Tenga en cuenta que este bucle es muy denso , alrededor de 1 byte por instrucción (dado que las instrucciones de nop son de 1 byte cada una), por lo que dispararemos las> 18 instrucciones en una condición de bloque 32B tan pronto como golpee 19 instrucciones en el bucle. En base al 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 provienen del LSD 5 hasta e incluyendo el bucle de 18 uop, pero a 19 uups y superiores, el 100% vienen del decodificador heredado.

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

Cyles / iteración para bucles con un tamaño dado

Los puntos azules son los bucles que encajan en el LSD y muestran un comportamiento algo complejo. Veremos esto más tarde.

Los puntos rojos (que comienzan en 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, para el decodificador heredado al menos, la observación de Peter se mantiene exactamente en Skylake: los bucles con un múltiplo de 4 uops pueden ejecutarse en un IPC de 4, pero cualquier otro número de uops perderá 1, 2 o 3 ranuras de ejecución (para bucles con Instrucciones 4N+3 , 4N+2 , 4N+1 , respectivamente).

No me queda claro por qué sucede esto. Aunque puede parecer obvio si considera que la deencoding ocurre en fragmentos contiguos 16B, y así a una velocidad de desencoding de 4 uops / ciclo de bucles no un múltiplo de 4 siempre tendrá algunas ranuras finales (desperdiciadas) en el ciclo se encuentra la instrucción jnz . Sin embargo, la unidad de captación y deencoding real se compone de fases de predeencoding y deencoding, con una cola intermedia. La fase de predeencoding 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 ocurre al final del ciclo puede ser absorbida por el predecodificador -> cola de deencoding ya que el predecodificador tiene un rendimiento promedio superior a 4.

Así que no puedo explicarlo completamente en base a mi comprensión de cómo funciona el predecodificador. Puede ser que exista alguna limitación adicional en la deencoding o la deencoding previa que impida la cuenta de ciclos no integrales. Por ejemplo, quizás los decodificadores heredados no pueden descodificar instrucciones en ambos lados de un salto, incluso si las instrucciones después del salto están disponibles en la cola predecodificada. Quizás esté relacionado con la necesidad de manejar la 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 añadida 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):

Ciclo / iteración del decodificador heredado cuando está desalineado

La mayoría de los tamaños de bucle ahora sufren una penalización de 1 o 2 ciclos. El caso de penalización 1 tiene sentido cuando considera decodificar límites de 16B y 4 instrucciones por deencoding de ciclo, y los casos de penalización de 2 ciclos ocurren para bucles donde por alguna razón el DSB se usa para 1 instrucción en el bucle (probablemente la instrucción de dec que aparece en su propio fragmento de 32 bytes), y se incurre en algunas penalizaciones de conmutación 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 en su valor nominal, parecería que, como se indicó anteriormente, deberían poder ocultar una burbuja de búsqueda de desalineación, pero no sucede (tal vez la cola no es lo suficientemente grande).

DSB (caché Uop)

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

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

 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 bucle probados enviados desde el DSB, el número de ciclos requeridos era N/4 , es decir, los bucles ejecutados con el rendimiento teórico máximo, incluso si no tenían un múltiplo de 4 uops. Por lo tanto, en general, en Skylake, los bucles de tamaño moderado servidos desde el DSB no deberían tener que preocuparse por garantizar que el conteo de uop coincida con un determinado múltiplo.

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

Ciclo cuenta para bucles servidos fuera del DSB

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

LSD (Detector de vapor en bucle)

Nota importante: Intel aparentemente ha desactivado el LSD en los chips Skylake (erratum SKL150) y Kaby Lake (KBL095, erratum KBW095) a través de una actualización de microcódigo y en Skylake-X, debido a un error relacionado con la interacción entre hyperthreading y el LSD. Para esos chips, el siguiente gráfico 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 continuo puede almacenar pequeños bucles 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 se mencionan las desventajas de rendimiento al usar el LSD.

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

Ciclos por iteración para bucles residentes en LSD

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

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

Entre los tamaños de bucle de 57 y 62 uops, el número de uops enviados desde LSD muestra un comportamiento extraño: aproximadamente el 70% de los uops se entregan desde el LSD, y el rest desde el DSB. Skylake nominalmente tiene un LSD de 64 uop, por lo que este es un tipo de transición justo antes de que se exceda el tamaño del LSD; quizás haya algún tipo de alineación interna dentro del IDQ (en el que se implementa el LSD) que solo cause impactos parciales al LSD en esta fase. Esta fase es corta y, en cuanto a rendimiento, parece ser principalmente una combinación lineal del rendimiento LSD completo que lo precede y el rendimiento completo en DSB que lo sigue.

Miremos 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 sobre una iteración de bucle único 8 . El rango es lo suficientemente corto como para que sea 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 en la siguiente región).

Un bucle de 10 uops, por otro lado, se ejecuta en 2,66 ciclos por iteración, convirtiéndolo en el único bucle de tamaño uniforme que no se ejecuta de forma óptima hasta que alcanza tamaños de bucle de 34 uops o superiores (distintos del valor atípico en 26) . Eso corresponde a algo así como una tasa de ejecución de uop / ciclo repetida de 4, 4, 4, 3 . Para un bucle de 5 uops, obtienes 1.33 ciclos por iteración, muy cerca pero no el mismo que el ideal de 1.25. Eso corresponde a una tasa de ejecución de 4, 4, 4, 4, 3 .

Realmente no puedo explicar estos resultados más que decir que hay algo complejo en marcha, tal vez unro. Los resultados son repetibles de ejecución a ejecución y robustos a los cambios, como el intercambio de nop por una instrucción que realmente hace algo como mov ecx, 123 .

Bucles de 11 a 32-uops: vemos un patrón de escalera, 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 pierden una “ranura de problema” y toman el mismo número de ciclos que un bucle con un uops más (es decir, un bucle de 17 uop toma los mismos 4.5 ciclos que un bucle de 18 uop). Así que 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 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 de iteración, a menos que tenga un período gigante, por lo que probablemente sea otra cosa.

El comportamiento general aquí es exactamente coherente (fuera de la anomalía 25/26) con el hardware desenrollando el ciclo por un factor de 2.

Lazos de 33 a ~ 64 uops: Vemos un patrón de escalera nuevamente, pero con un período de 4 y peor desempeño promedio 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 los bucles de 32 a 64 uops, el LSD no proporciona ningún beneficio aparente sobre los decodificadores heredados, en términos del rendimiento del front-end para esta limitación particular . Por supuesto, hay muchas otras formas en que el LSD es mejor: evita muchos de los potenciales cuellos de botella de deencoding que ocurren 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 el caché uop en general funcionan mejor en el frente que los bucles entregados desde el LSD, a pesar de que el LSD normalmente se posiciona como una fuente de uops estrictamente mejor que el DSB (por ejemplo, como parte de un consejo 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 recuento uop dado, frente al rendimiento teórico máximo de 4 uops por ciclo. Un golpe de eficiencia del 10% significa que solo tiene el 90% del rendimiento que calcularía con la fórmula simple N/4 .

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

Pérdida de eficiencia por tamaño de bucle

Las tres regiones discutidas anteriormente están coloreadas de forma 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 golpe 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 se 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 la peor cuenta general de uop.

Alineación

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

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

Ciclos desalineados por iteración

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

Haswell

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

LSD + Oleoducto de desencoding heredado

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

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

Haswell vs Skylake LSD entrega%

Como se describió anteriormente, el ciclo Skylake deja de ser entregado desde el LSD a exactamente 19 uops, como se esperaba desde el límite de código de 18 uop por 32 bytes. Haswell, por otro lado, parece dejar de brindar confiadamente desde el LSD para los bucles 16 uop y 17 uop. No tengo ninguna explicación para esto. También hay una diferencia en el caso 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, en su mayoría nos preocupamos por el rendimiento real, ¿verdad? Así que echemos un vistazo a los ciclos / iteración para el caso del código denso alineado de 32 bytes:

Haswell vs Skylake LSD + Legacy Pipeline

Esta es la misma información que se muestra arriba para Skylake (la serie desalineada ha sido eliminada), con Haswell graficado al costado. Inmediatamente observa que el patrón es similar para Haswell, pero no es el mismo. Como arriba, hay dos regiones aquí:

Deencoding heredada

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

Para el rango de 19 a 30 uops son idénticos, pero después de eso Haswell rompe el patrón. Skylake tomó ciclos ceil(N/4) para 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), dichos bucles son (localmente) el conteo menos óptimo. (al menos localmente) – se necesita un ciclo más para ejecutar esos bucles que Skylake. Así que en realidad es mejor con bucles de 4N-1 uops en Haswell, excepto que el 25% de dichos bucles que también son de la forma 16-1N (31, 47, 63, etc.) toman un ciclo adicional. Está empezando a parecerse a un cálculo de año bisiesto, pero es probable que el patrón se entienda mejor visualmente más arriba.

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

 0000000000455a80 : 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  

Aquí he observado el fragmento de deencoding de 16B (1-3) en el que aparece cada instrucción y el ciclo en el que se decodificará. La regla es básicamente que hasta las próximas 4 instrucciones se decodifican, siempre y cuando caigan en el fragmento actual 16B. De lo contrario, tienen que esperar hasta el próximo ciclo. Para N = 35, vemos que hay una pérdida de 1 ranura de deencoding en el ciclo 4 (solo quedan 3 instrucciones en el fragmento 16B), pero 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 vista truncada en N = 36, que es idéntica, excepto por el final del ciclo:

 0000000000455b20 : 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  

Ahora hay 5 instrucciones para decodificar en el 3er y último bloque de 16B, por lo que se necesita un ciclo adicional. Básicamente 35 instrucciones, para este patrón particular de instrucciones, se alinean mejor con los límites de 16B bit y guarda un ciclo cuando se decodifica. ¡Esto no significa que N = 35 es 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 también explica 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  

Aquí la jne final se ha deslizado en 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.

De modo que los resultados del decodificador heredado Haswell se explican perfectamente por un decodificador heredado que se comporta como se describe, por ejemplo, en el documento de microarchitecture de Agner Fog. De hecho, también parece explicar los resultados de Skylake si usted supone que Skylake puede decodificar 5 instrucciones por ciclo (entregando hasta 5 uops) 9 . Asumiendo que sí, el legado asintótico de deencoding de este código para Skylake sigue siendo 4-uops, ya que un bloque de 16 nudos decodifica 5-5-5-1, contra 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 todas las 5 instrucciones restantes, contra 4-1 para Haswell, guardando un ciclo.

El resultado es que parece que el comportamiento del decodificador heredado se puede entender de una manera bastante directa, y el principal consejo de optimización es seguir masajeando el código para que caiga “inteligentemente” en los segmentos alineados 16B (quizás sea NP- duro como el embalaje bin?).

DSB (y LSD de nuevo)

A continuación, echemos un vistazo al escenario donde se sirve el código del LSD o DSB, utilizando la prueba de “nop largo” que evita romper el límite de 18 uop por fragmento de 32B, y así permanece en el DSB.

Haswell vs Skylake:

Haswell vs Skylake LSD y DSB

Tenga en cuenta el comportamiento del LSD: aquí Haswell deja de atender el LSD a exactamente 57 uops, lo que es completamente coherente con el tamaño publicado del LSD de 57 uops. No hay un “período de transición” extraño como el que vemos en Skylake. Haswell también tiene el extraño comportamiento de 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 es capaz de mantener 4 uops / ciclo incluso cuando eso sea resultados en un número no entero de ciclos. La ligera desviación de lo esperado en 25 y 26 uops ha desaparecido. Tal vez la tasa de entrega de 6-uop de Skylake le ayude a evitar problemas de alineación de la memoria caché u-up que Haswell sufre con su tasa de entrega de 4 uop.

Se necesita ayuda

Se necesita ayuda para ver si estos resultados son válidos para las architectures más antiguas y nuevas, y para confirmar si esto es cierto en Skylake. 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 memoria caché uop aumentó de 4 a 6. Ambos podrían afectar los resultados que se describen aquí.

1 A Intel realmente le gusta llamar al decodificador heredado el MITE (Micro-instruction Translation Engine), tal vez porque es un paso en falso etiquetar cualquier parte de su architecture con la connotación heredada .

2 Técnicamente hay otra fuente de uops aún más lenta: la MS (motor de secuenciación de microcódigos), que se usa para implementar cualquier instrucción con más de 4 uops, pero ignoramos esto porque 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 hasta 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é uop. Probablemente es raro encontrar esta condición en la práctica, ya que el código necesita ser muy denso (menos de 2 bytes por instrucción) para activar esto.

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

5 El LSD es el detector de stream de bucle , que almacena pequeños bucles de hasta 64 (Skylake) uops directamente en el IDQ. En architectures 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 instrucciones de cero, lo que significa que las instrucciones dec y jnz se fusionarían en macro, con un cambio correspondiente en el conteo uop. Solo tome mi palabra de que todos los bucles con 4 o menos uops se ejecutan en el mejor de 1 ciclo / iteración.

7 Para divertirme, acabo de ejecutar una perf stat en una corta tirada de Firefox, donde abrí una pestaña y hice clic en algunas preguntas sobre el Desbordamiento de stack. Para las instrucciones entregadas, obtuve un 46% de DSB, un 50% del descodificador heredado y un 4% para LSD. Esto muestra que, al menos para un código grande y ramificado como un navegador, el DSB aún no puede capturar la gran mayoría del código (afortunadamente, los decodificadores heredados no son tan malos).

8 Con esto, quiero decir que todos los demás recuentos de ciclos se pueden explicar simplemente tomando un costo de ciclo integral “efectivo” en uops (que puede ser más alto que el tamaño real es uops) y dividiéndolo por 4. Para estos bucles muy cortos , esto no funciona: no puede obtener 1.333 ciclos por iteración dividiendo cualquier número por 4. Dicho de otra manera, en todas las demás regiones, los costos tienen la forma N / 4 para un 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 .