assembly x86 intel cpu-architecture micro-optimization

assembly - ¿Cómo funcionan exactamente los registros parciales en Haswell/Skylake? Escribir AL parece tener una dependencia falsa en RAX, y AH es inconsistente



x86 intel (2)

Otras respuestas son bienvenidas para abordar Sandybridge e IvyBridge con más detalle. No tengo acceso a ese hardware.

No he encontrado ninguna diferencia de comportamiento de registro parcial entre HSW y SKL. En Haswell y Skylake, todo lo que he probado hasta ahora es compatible con este modelo:

AL nunca se renombra por separado de RAX (o r15b de r15). Entonces, si nunca toca los registros high8 (AH / BH / CH / DH), todo se comporta exactamente como en una CPU sin cambio de nombre de registro parcial (por ejemplo, AMD).

El acceso de solo escritura a AL se fusiona con RAX, con una dependencia de RAX. Para cargas en AL, esta es una carga de ALU + micro fusionada que se ejecuta en p0156, que es una de las pruebas más sólidas de que realmente se está fusionando en cada escritura, y no solo haciendo una doble contabilidad elegante como especuló Agner.

Agner (e Intel) dicen que Sandybridge puede requerir una fusión uop para AL, por lo que probablemente se renombra por separado de RAX. Para SnB, el manual de optimización de Intel (sección 3.5.2.4 Puestos de registro parcial) dice

SnB (no necesariamente uarches posteriores) inserta una fusión uop en los siguientes casos:

  • Después de escribir en uno de los registros AH, BH, CH o DH y antes de una siguiente lectura de la forma de 2, 4 u 8 bytes del mismo registro. En estos casos, se inserta una fusión micro-op. La inserción consume un ciclo de asignación completo en el que no se pueden asignar otras microoperaciones.

  • Después de una microoperación con un registro de destino de 1 o 2 bytes, que no es una fuente de la instrucción (o la forma más grande del registro), y antes de la siguiente lectura de una forma de 2, 4 u 8 bytes del mismo registro En estos casos, la fusión micro-op es parte del flujo .

Creo que dicen que en SnB, add al,bl RMW RMX completo en lugar de renombrarlo por separado, porque uno de los registros de origen es (parte de) RAX. Supongo que esto no se aplica a una carga como mov al, [rbx + rax] ; rax en un modo de direccionamiento probablemente no cuenta como fuente.

No he probado si high8 fusionando uops todavía tiene que emitir / cambiar el nombre por su cuenta en HSW / SKL. Eso haría que el impacto frontal sea equivalente a 4 uops (ya que ese es el problema / cambiar el nombre del ancho de la tubería).

  • No hay forma de romper una dependencia que involucra a AL sin escribir EAX / RAX. xor al,al no ayuda, y tampoco lo hace mov al, 0 .
  • movzx ebx, al tiene latencia cero (renombrado) y no necesita unidad de ejecución. (es decir, mov-elimination funciona en HSW y SKL). Activa la fusión de AH si está sucio , lo que supongo que es necesario para que funcione sin una ALU. Probablemente no sea una coincidencia que Intel haya reducido el cambio de nombre en el mismo lugar que introdujo la eliminación de mov. (La guía de microarcos de Agner Fog tiene un error aquí, diciendo que los movimientos de extensión cero no se eliminan en HSW o SKL, solo en IvB).
  • movzx eax, al no se elimina al cambiar el nombre. mov-elimination en Intel nunca funciona para lo mismo, lo mismo. mov rax,rax tampoco se elimina, aunque no tiene que extender nada a cero. (Aunque no tendría sentido darle soporte especial de hardware, porque es solo un no-op, a diferencia de mov eax,eax ). De todos modos, prefiera moverse entre dos registros arquitectónicos separados cuando se extienda a cero, ya sea con un mov 32 bits o un movzx 8 bits.
  • movzx eax, bx no se elimina al cambiar el nombre en HSW o SKL. Tiene una latencia de 1c y usa una ALU uop. El manual de optimización de Intel solo menciona la latencia cero para movzx de 8 bits (y señala que movzx r32, high8 nunca cambia de nombre).

Los registros High-8 se pueden renombrar por separado del resto del registro, y es necesario fusionar uops.

  • Acceso de solo escritura a ah con mov ah, r8 o mov ah, [mem] cambia el nombre de AH, sin dependencia del valor anterior. Ambas son instrucciones que normalmente no necesitarían una ALU uop (para la versión de 32 bits).
  • un RMW de AH (como inc ah ) lo ensucia.
  • setcc ah depende del viejo ah , pero aún lo ensucia. Creo que mov ah, imm8 es lo mismo, pero no he probado tantos casos de esquina.

    (Inexplicable: un bucle que involucra setcc ah veces puede ejecutarse desde el LSD, vea el bucle rcr al final de esta publicación. Tal vez mientras ah esté limpio al final del bucle, ¿puede usar el LSD?).

    Si ah está sucio, setcc ah fusiona en el ah renombrado, en lugar de forzar una fusión en rax . Por ejemplo, %rep 4 ( inc al / test ebx,ebx / setcc ah / inc al / inc ah ) no genera uops de fusión y solo se ejecuta en aproximadamente 8.7c (la latencia de 8 inc al ralentiza por conflictos de recursos de los uops para ah También la cadena inc ah / setcc ah dep).

    Creo que lo que está sucediendo aquí es que setcc r8 siempre se implementa como lectura-modificación-escritura. Intel probablemente decidió que no valía la pena tener un setcc uop de solo setcc para optimizar el caso de setcc ah , ya que es muy raro que el código generado por el compilador setcc ah . (Pero vea el enlace godbolt en la pregunta: clang4.0 con -m32 lo hará).

  • la lectura de AX, EAX o RAX desencadena una fusión uop (que ocupa el ancho de banda del problema de front-end / cambio de nombre). Probablemente, la RAT (tabla de asignación de registros) rastrea el estado alto de 8 sucios para la arquitectura R [ABCD] X, e incluso después de que se retira una escritura en AH, los datos de AH se almacenan en un registro físico separado de RAX. Incluso con 256 NOP entre escribir AH y leer EAX, hay una fusión adicional. (Tamaño ROB = 224 en SKL, por lo que esto garantiza que el mov ah, 123 fue retirado). Detectado con uops_issued / ejecutados contadores de rendimiento, que muestran claramente la diferencia.

  • La lectura-modificación-escritura de AL (por ejemplo, inc al ) se combina de forma gratuita, como parte de la UOP ALU. (Solo probado con unos pocos uops simples, como add / inc , no div r8 o mul r8 ). Una vez más, no se activa la fusión uop incluso si AH está sucio.

  • Escribir solo en EAX / RAX (como lea eax, [rsi + rcx] o xor eax,eax ) borra el estado AH-sucio (sin fusión uop).

  • Escribir solo en AX ( mov ax, 1 ) activa primero una fusión de AH. Supongo que en lugar de una carcasa especial, funciona como cualquier otro RMW de AX / RAX. (TODO: prueba mov ax, bx , aunque eso no debería ser especial porque no ha cambiado de nombre).
  • xor ah,ah tiene una latencia de 1c, no es de última generación y aún necesita un puerto de ejecución.
  • La lectura y / o escritura de AL no fuerza una fusión, por lo que AH puede permanecer sucio (y ser usado independientemente en una cadena de dep separada). (por ejemplo, add ah, cl / add al, dl puede ejecutarse a 1 por reloj (con cuellos de botella en la latencia de agregar).

Hacer que AH se ensucie evita que se ejecute un bucle desde el LSD (el búfer de bucle), incluso cuando no hay uops de fusión. El LSD es cuando la CPU recicla uops en la cola que alimenta la etapa de emisión / cambio de nombre. (Llamado IDQ).

Insertar Uops de fusión es un poco como insertar Uops de sincronización de pila para el motor de pila. El manual de optimización de Intel dice que el LSD de SnB no puede ejecutar bucles con push / pop no coincidentes, lo que tiene sentido, pero implica que puede ejecutar bucles con push / pop equilibrado. Eso no es lo que estoy viendo en SKL: incluso el push / pop equilibrado evita que se ejecute desde el LSD (por ejemplo, push rax / pop rdx / times 6 imul rax, rdx . (Puede haber una diferencia real entre el LSD de SnB y el HSW / SKL: SnB puede simplemente "bloquear" los uops en el IDQ en lugar de repetirlos varias veces, por lo que un ciclo de 5 uop tarda 2 ciclos en emitirse en lugar de 1.25 .) De todos modos, parece que HSW / SKL no puede usar el LSD cuando un registro de 8 altos está sucio o cuando contiene uops de motor de pila.

Este comportamiento puede estar relacionado con una errata en SKL :

SKL150: Los bucles cortos que utilizan registros AH / BH / CH / DH pueden causar un comportamiento impredecible del sistema

Problema: en condiciones complejas de microarquitectura, los bucles cortos de menos de 64 instrucciones que usan registros AH, BH, CH o DH, así como sus registros más amplios correspondientes (por ejemplo, RAX, EAX o AX para AH) pueden causar un comportamiento impredecible del sistema . Esto solo puede suceder cuando ambos procesadores lógicos en el mismo procesador físico están activos.

Esto también puede estar relacionado con la declaración del manual de optimización de Intel de que SnB al menos tiene que emitir / cambiar el nombre de una fusión de AH en un ciclo por sí mismo. Esa es una diferencia extraña para el front-end.

Mi registro del kernel de Linux dice microcode: sig=0x506e3, pf=0x2, revision=0x84 . El paquete intel-ucode Arch Linux solo proporciona la actualización, debe editar los archivos de configuración para que realmente se cargue . Así que mi prueba de Skylake fue en un i7-6700k con revisión de microcódigo 0x84, que no incluye la solución para SKL150 . Coincide con el comportamiento de Haswell en todos los casos que probé, IIRC. (por ejemplo, tanto Haswell como mi SKL pueden ejecutar el setne ah / add ah,ah / rcr ebx,1 / mov eax,ebx loop desde el LSD). Tengo HT habilitado (que es una condición previa para que se manifieste SKL150), pero estaba probando en un sistema inactivo en su mayoría, por lo que mi hilo tenía el núcleo en sí mismo.

Con el microcódigo actualizado, el LSD está completamente deshabilitado para todo todo el tiempo, no solo cuando los registros parciales están activos. lsd.uops siempre es exactamente cero, incluso para programas reales, no bucles sintéticos. Los errores de hardware (en lugar de los errores de microcódigo) a menudo requieren deshabilitar una función completa para solucionarlo. Esta es la razón por la cual se informa que SKL-avx512 (SKX) no tiene un búfer de bucle invertido . Afortunadamente, este no es un problema de rendimiento: el aumento del rendimiento de la caché uop-cache de SKL sobre Broadwell casi siempre puede mantenerse al día con el problema / cambio de nombre.

Latencia adicional AH / BH / CH / DH:

  • Leer AH cuando no está sucio (renombrado por separado) agrega un ciclo adicional de latencia para ambos operandos. por ejemplo, add bl, ah tiene una latencia de 2c desde la entrada BL hasta la salida BL, por lo que puede agregar latencia a la ruta crítica incluso si RAX y AH no son parte de ella. (He visto este tipo de latencia adicional para el otro operando antes, con latencia vectorial en Skylake, donde un retraso int / float "contamina" un registro para siempre. TODO: escriba eso).

Esto significa desempaquetar bytes con movzx ecx, al / movzx edx, ah tiene latencia extra vs. movzx / shr eax,8 / movzx , pero aún mejor rendimiento.

  • Leer AH cuando está sucio no agrega ninguna latencia. ( add ah,ah o add ah,dh / add dh,ah tienen 1c de latencia por adición). No he hecho muchas pruebas para confirmar esto en muchos casos de esquina.

    Hipótesis: un valor high8 sucio se almacena en la parte inferior de un registro físico . Leer un high8 limpio requiere un cambio para extraer bits [15: 8], pero leer un high8 sucio puede tomar bits [7: 0] de un registro físico como una lectura normal de registro de 8 bits.

La latencia adicional no significa un rendimiento reducido. Este programa puede ejecutarse a 1 iter por 2 relojes, a pesar de que todas las instrucciones de add tienen una latencia de 2c (desde la lectura de DH, que no se modifica).

global _start _start: mov ebp, 100000000 .loop: add ah, dh add bh, dh add ch, dh add al, dh add bl, dh add cl, dh add dl, dh dec ebp jnz .loop xor edi,edi mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h syscall ; sys_exit_group(0)

Performance counter stats for ''./testloop'': 48.943652 task-clock (msec) # 0.997 CPUs utilized 1 context-switches # 0.020 K/sec 0 cpu-migrations # 0.000 K/sec 3 page-faults # 0.061 K/sec 200,314,806 cycles # 4.093 GHz 100,024,930 branches # 2043.675 M/sec 900,136,527 instructions # 4.49 insn per cycle 800,219,617 uops_issued_any # 16349.814 M/sec 800,219,014 uops_executed_thread # 16349.802 M/sec 1,903 lsd_uops # 0.039 M/sec 0.049107358 seconds time elapsed

Algunos cuerpos de bucle de prueba interesantes :

%if 1 imul eax,eax mov dh, al inc dh inc dh inc dh ; add al, dl mov cl,dl movzx eax,cl %endif Runs at ~2.35c per iteration on both HSW and SKL. reading `dl` has no dep on the `inc dh` result. But using `movzx eax, dl` instead of `mov cl,dl` / `movzx eax,cl` causes a partial-register merge, and creates a loop-carried dep chain. (8c per iteration). %if 1 imul eax, eax imul eax, eax imul eax, eax imul eax, eax imul eax, eax ; off the critical path unless there''s a false dep %if 1 test ebx, ebx ; independent of the imul results ;mov ah, 123 ; dependent on RAX ;mov eax,0 ; breaks the RAX dependency setz ah ; dependent on RAX %else mov ah, bl ; dep-breaking %endif add ah, ah ;; ;inc eax ; sbb eax,eax rcr ebx, 1 ; dep on add ah,ah via CF mov eax,ebx ; clear AH-dirty ;; mov [rdi], ah ;; movzx eax, byte [rdi] ; clear AH-dirty, and remove dep on old value of RAX ;; add ebx, eax ; make the dep chain through AH loop-carried %endif

La versión setcc (con el %if 1 ) tiene una latencia transportada en bucle de 20c, y se ejecuta desde el LSD a pesar de que tiene setcc ah y add ah,ah .

00000000004000e0 <_start.loop>: 4000e0: 0f af c0 imul eax,eax 4000e3: 0f af c0 imul eax,eax 4000e6: 0f af c0 imul eax,eax 4000e9: 0f af c0 imul eax,eax 4000ec: 0f af c0 imul eax,eax 4000ef: 85 db test ebx,ebx 4000f1: 0f 94 d4 sete ah 4000f4: 00 e4 add ah,ah 4000f6: d1 db rcr ebx,1 4000f8: 89 d8 mov eax,ebx 4000fa: ff cd dec ebp 4000fc: 75 e2 jne 4000e0 <_start.loop> Performance counter stats for ''./testloop'' (4 runs): 4565.851575 task-clock (msec) # 1.000 CPUs utilized ( +- 0.08% ) 4 context-switches # 0.001 K/sec ( +- 5.88% ) 0 cpu-migrations # 0.000 K/sec 3 page-faults # 0.001 K/sec 20,007,739,240 cycles # 4.382 GHz ( +- 0.00% ) 1,001,181,788 branches # 219.276 M/sec ( +- 0.00% ) 12,006,455,028 instructions # 0.60 insn per cycle ( +- 0.00% ) 13,009,415,501 uops_issued_any # 2849.286 M/sec ( +- 0.00% ) 12,009,592,328 uops_executed_thread # 2630.307 M/sec ( +- 0.00% ) 13,055,852,774 lsd_uops # 2859.456 M/sec ( +- 0.29% ) 4.565914158 seconds time elapsed ( +- 0.08% )

Inexplicable: se ejecuta desde el LSD, aunque ensucia AH. (Al menos creo que sí. TODO: intente agregar algunas instrucciones que hagan algo con eax antes del mov eax,ebx borra).

Pero con mov ah, bl , se ejecuta en 5.0c por iteración (cuello de botella de rendimiento total) en ambos HSW / SKL. (La tienda / recarga comentada también funciona, pero SKL tiene un reenvío de tiendas más rápido que HSW, y es de variable-latency ...)

# mov ah, bl version 5,009,785,393 cycles # 4.289 GHz ( +- 0.08% ) 1,000,315,930 branches # 856.373 M/sec ( +- 0.00% ) 11,001,728,338 instructions # 2.20 insn per cycle ( +- 0.00% ) 12,003,003,708 uops_issued_any # 10275.807 M/sec ( +- 0.00% ) 11,002,974,066 uops_executed_thread # 9419.678 M/sec ( +- 0.00% ) 1,806 lsd_uops # 0.002 M/sec ( +- 3.88% ) 1.168238322 seconds time elapsed ( +- 0.33% )

Tenga en cuenta que ya no se ejecuta desde el LSD.

Este ciclo se ejecuta en una iteración por 3 ciclos en Intel Conroe / Merom, con un cuello de botella en el rendimiento total como se esperaba. Pero en Haswell / Skylake, se ejecuta en una iteración por 11 ciclos, aparentemente porque setnz al depende de la última imul .

; synthetic micro-benchmark to test partial-register renaming mov ecx, 1000000000 .loop: ; do{ imul eax, eax ; a dep chain with high latency but also high throughput imul eax, eax imul eax, eax dec ecx ; set ZF, independent of old ZF. (Use sub ecx,1 on Silvermont/KNL or P4) setnz al ; ****** Does this depend on RAX as well as ZF? movzx eax, al jnz .loop ; }while(ecx);

Si setnz al depende de rax , la secuencia 3ximul / setcc / movzx forma una cadena de dependencia transportada en bucle. Si no, cada cadena imul setcc / movzx / 3x es independiente, se bifurca a partir del dec que actualiza el contador de bucle. El 11c por iteración medido en HSW / SKL se explica perfectamente por un cuello de botella de latencia: 3x3c (imul) + 1c (lectura-modificación-escritura por setcc) + 1c (movzx dentro del mismo registro).

Fuera del tema: evitar estos cuellos de botella (intencionales)

Estaba buscando un comportamiento comprensible / predecible para aislar cosas de registro parcial, no un rendimiento óptimo.

Por ejemplo, xor -zero / set-flags / setcc es mejor de todos modos (en este caso, xor eax,eax / dec ecx / setnz al ). Eso rompe el dep en eax en todas las CPU (excepto las primeras familias de P6 como PII y PIII), aún evita las penalizaciones por fusión de registros parciales y ahorra 1c de latencia movzx . También utiliza una UOP de ALU menos en las CPU que manejan la reducción a cero en la etapa de cambio de nombre de registro . Consulte ese enlace para obtener más información sobre el uso de xor-zeroing con setcc .

Tenga en cuenta que AMD, Intel Silvermont / KNL y P4 no hacen ningún cambio de nombre en el registro parcial. Es solo una característica de las CPU de la familia Intel P6 y su descendiente, la familia Intel Sandybridge, pero parece que se está eliminando gradualmente.

desafortunadamente, gcc tiende a usar cmp / setcc al / movzx eax,al donde podría haber usado xor lugar de movzx (ejemplo del compilador-explorador Godbolt) , mientras que clang usa xor-zero / cmp / setcc a menos que combine múltiples condiciones booleanas como count += (a==b) | (a==~b) count += (a==b) | (a==~b) .

La versión xor / dec / setnz se ejecuta a 3.0c por iteración en Skylake, Haswell y Core2 (con cuellos de botella en el rendimiento total). xor zeroing rompe la dependencia del valor anterior de eax en todas las CPU fuera de servicio que no sean PPro / PII / PIII / early-Pentium-M (donde aún evita penalizaciones por fusión de registro parcial pero no rompe el dep ) La guía de microarquitectura de Agner Fog describe esto . Reemplazando la reducción a cero con mov eax,0 ralentiza a uno por 4.78 ciclos en Core2: pérdida de 2-3c (¿en el extremo frontal?) Para insertar una unión parcial de registro parcial cuando Imul lee eax después de la setnz al .

Además, utilicé movzx eax, al que derrota la eliminación de mov, tal como lo hace mov rax,rax . (IvB, HSW y SKL pueden cambiar el nombre de movzx eax, bl con latencia 0, pero Core2 no). Esto hace que todo sea igual en Core2 / SKL, excepto el comportamiento de registro parcial.

El comportamiento de Core2 es consistente con la guía de microarquitectura de Agner Fog , pero el comportamiento de HSW / SKL no lo es. De la sección 11.10 para Skylake, y lo mismo para uarches anteriores de Intel:

Se pueden almacenar diferentes partes de un registro de propósito general en diferentes registros temporales para eliminar dependencias falsas.

Desafortunadamente, no tiene tiempo para realizar pruebas detalladas para cada nuevo grupo para volver a probar los supuestos, por lo que este cambio de comportamiento se escapó por las grietas.

Agner describe la inserción de una uop de fusión (sin bloqueo) para registros high8 (AH / BH / CH / DH) en Sandybridge a través de Skylake, y para low8 / low16 en SnB. (Desafortunadamente, he estado difundiendo información errónea en el pasado y diciendo que Haswell puede fusionar AH de forma gratuita. Leí la sección Haswell de Agner demasiado rápido, y no noté el párrafo posterior sobre registros de high8. Avíseme si ve mis comentarios incorrectos en otras publicaciones, por lo que puedo eliminarlos o agregar una corrección. Intentaré al menos encontrar y editar mis respuestas donde he dicho esto).

Mis preguntas reales: ¿Cómo se comportan realmente los registros parciales en Skylake?

¿Es todo igual desde IvyBridge hasta Skylake, incluida la latencia extra high8?

El manual de optimización de Intel no es específico sobre qué CPU tienen dependencias falsas para qué (aunque menciona que algunas CPU las tienen), y omite cosas como leer AH / BH / CH / DH (registros de high8) agregando latencia adicional incluso cuando no tienen No ha sido modificado.

Si hay algún comportamiento de la familia P6 (Core2 / Nehalem) que la guía de microarquitectura de Agner Fog no describe, eso también sería interesante, pero probablemente debería limitar el alcance de esta pregunta a solo Skylake o Sandybridge-family.

Mis datos de prueba de Skylake , desde poner %rep 4 secuencias cortas dentro de un pequeño dec ebp/jnz que ejecuta iteraciones de 100M o 1G. Medí ciclos con Linux perf la misma manera que en mi respuesta aquí , en el mismo hardware (computadora de escritorio Skylake i7 6700k).

A menos que se indique lo contrario, cada instrucción se ejecuta como 1 uop de dominio fusionado, utilizando un puerto de ejecución ALU. (Medido con ocperf.py stat -e ...,uops_issued.any,uops_executed.thread ). Esto detecta (ausencia de) eliminación de movimientos y uops adicionales de fusión.

Los casos "4 por ciclo" son una extrapolación al caso infinitamente desenrollado. La sobrecarga del bucle ocupa parte del ancho de banda del front-end, pero algo mejor que 1 por ciclo es una indicación de que el cambio de nombre del registro evitó la dependencia de salida de escritura tras escritura , y que el uop no se maneja internamente como una modificación de lectura -escribir.

Escribir solo en AH : evita que el bucle se ejecute desde el búfer de bucle invertido (también conocido como Loop Stream Detector (LSD)). Los recuentos para lsd.uops son exactamente 0 en HSW, y pequeños en SKL (alrededor de 1.8k) y no se escalan con el recuento de iteraciones de bucle. Probablemente esos recuentos sean de algún código del núcleo. Cuando los bucles se ejecutan desde el LSD, lsd.uops ~= uops_issued dentro del ruido de medición. Algunos bucles alternan entre LSD o no LSD (p. Ej., Cuando podrían no encajar en la memoria caché uop si la decodificación comienza en el lugar incorrecto), pero no me encontré con eso mientras lo probaba.

  • mov ah, bh y / o mov ah, bl repetidos mov ah, bl corre a 4 por ciclo. Se necesita una ALU uop, por lo que no se elimina como mov eax, ebx .
  • mov ah, [rsi] repetido mov ah, [rsi] ejecuta a 2 por ciclo (cuello de botella de rendimiento de carga).
  • mov ah, 123 repetido mov ah, 123 carreras a 1 por ciclo. (Un xor eax,eax dep- xor eax,eax dentro del bucle elimina el cuello de botella).
  • setz ah o setc ah ejecutan a 1 por ciclo. (Un xor eax,eax última xor eax,eax permite embotellar el rendimiento de setcc para setcc y la rama de bucle).

    ¿Por qué escribir ah con una instrucción que normalmente usaría una unidad de ejecución ALU tiene una dependencia falsa del valor anterior, mientras que mov r8, r/m8 no (para reg o src de memoria)? (¿Y qué hay de mov r/m8, r8 ? Seguramente no importa cuál de los dos mov r/m8, r8 que utilices para los movimientos reg-reg?)

  • repetido add ah, 123 corre a 1 por ciclo, como se esperaba.

  • repetido add dh, cl corre a 1 por ciclo.
  • repetido add dh, dh corre a 1 por ciclo.
  • repetido add dh, ch funciona a 0.5 por ciclo. Leer [ABCD] H es especial cuando están "limpios" (en este caso, RCX no se ha modificado recientemente).

Terminología : Todos estos dejan AH (o DH) " sucio ", es decir, necesitan fusionarse (con una fusión uop) cuando se lee el resto del registro (o en algunos otros casos). es decir, se cambia el nombre de AH por separado de RAX, si lo entiendo correctamente. " limpio " es lo contrario. Hay muchas formas de limpiar un registro sucio, la más simple es inc eax o mov eax, esi .

Escribir solo en AL : estos bucles se ejecutan desde el LSD: uops_issue.any ~ = lsd.uops .

  • mov al, bl repetido mov al, bl corre a 1 por ciclo. Un xor eax,eax ocasional que rompe el dep xor eax,eax por grupo permite que el cuello de botella de ejecución de OOO afecte el rendimiento de UOP, no la latencia.
  • mov al, [rsi] repetidamente mov al, [rsi] ejecuta a 1 por ciclo, como un ALU + micro-fusionado uop de carga. (uops_issued = 4G + bucle de sobrecarga, uops_executed = 8G + bucle de sobrecarga). Un xor eax,eax última xor eax,eax antes de un grupo de 4 le permite embotellar 2 cargas por reloj.
  • mov al, 123 repetido mov al, 123 carreras a 1 por ciclo.
  • mov al, bh repetido mov al, bh corre a 0.5 por ciclo. (1 por 2 ciclos). Leer [ABCD] H es especial.
  • xor eax,eax + 6x mov al,bh + dec ebp/jnz : 2c por iter, cuello de botella en 4 uops por reloj para el front-end.
  • repetido add dl, ch funciona a 0.5 por ciclo. (1 por 2 ciclos). Leer [ABCD] H aparentemente crea latencia extra para dl .
  • repetido add dl, cl corre a 1 por ciclo.

Creo que una escritura en un registro de bajo 8 se comporta como una mezcla de RMW en el registro completo, como add eax, 123 sería, pero no desencadena una fusión si ah está sucio. Entonces (aparte de ignorar la fusión AH ) se comporta igual que en las CPU que no realizan ningún cambio de nombre de registro parcial. Parece que AL nunca se renombra por separado de RAX ?

  • inc al pares inc al / inc ah pueden ejecutarse en paralelo.
  • mov ecx, eax inserta una fusión uop si ah está "sucio", pero se cambia el nombre del mov real. Esto es lo que describe Agner Fog para IvyBridge y versiones posteriores.
  • repetido movzx eax, ah ejecuta a uno por 2 ciclos. (La lectura de registros de 8 altos después de escribir registros completos tiene latencia adicional).
  • movzx ecx, al tiene latencia cero y no toma un puerto de ejecución en HSW y SKL. (Al igual que lo que describe Agner Fog para IvyBridge, pero dice que HSW no cambia el nombre de movzx).
  • movzx ecx, cl tiene latencia 1c y toma un puerto de ejecución. ( mov-elimination nunca funciona para el same,same caso , solo entre diferentes registros arquitectónicos).

    ¿Un bucle que inserta una fusión uop cada iteración no puede ejecutarse desde el LSD (buffer de bucle)?

No creo que haya nada especial en AL / AH / RAX vs. B *, C *, DL / DH / RDX. He probado algunos con registros parciales en otros registros (aunque en su mayoría estoy mostrando AL / AH por consistencia), y nunca he notado ninguna diferencia.

¿Cómo podemos explicar todas estas observaciones con un modelo sensible de cómo funciona el microarch interno?

Relacionado: Los problemas de marca parcial son diferentes de los problemas de registro parcial. Vea la instrucción INC vs ADD 1: ¿Importa? para algunas cosas súper extrañas con shr r32,cl (e incluso shr r32,2 en Core2 / Nehalem: no lea las banderas de un turno que no sea por 1).

Consulte también Problemas con ADC / SBB e INC / DEC en bucles cerrados en algunas CPU para cosas de bandera parcial en bucles adc .


Actualización: Posible evidencia de que IvyBridge todavía cambia el nombre de los registros low16 / low8 por separado del registro completo, como Sandybridge pero a diferencia de Haswell y posteriores.

InstLatX64 resultados de InstLatX64 de SnB e IvB muestran un rendimiento de movsx r16, r8 para movsx r16, r8 (como se esperaba, movsx nunca se elimina y solo había 3 ALU antes de Haswell).

Pero al parecer, el movsx r16, r8 InstLat movsx r16, r8 prueba los cuellos de botella de Haswell / Broadwell / Skylake con un rendimiento de 1c (vea también este informe de error en el github instlat ) Probablemente escribiendo el mismo registro arquitectónico, creando una cadena de fusiones.

(El rendimiento real de esa instrucción con registros de destino separados es 0.25c en mi Skylake. Probado con 7 instrucciones movsx que escriben en eax..edi y r10w / r11w, todas leen desde cl . Y un dec ebp/jnz como la rama de bucle a haz un bucle de 8 uop.)

Si estoy acertando sobre lo que creó ese resultado de rendimiento 1c en las CPU después de IvB, está haciendo algo como ejecutar un bloque de movsx dx, al . Y eso solo puede ejecutarse a más de 1 IPC en CPU que dx nombre de dx separado de RDX en lugar de fusionarse. Entonces podemos concluir que IvB en realidad todavía cambia el nombre de los registros low8 / low16 por separado de los registros completos, y no fue hasta Haswell que lo descartaron. ( Pero aquí hay algo sospechoso: si esta explicación es correcta, deberíamos ver el mismo rendimiento de 1c en AMD que no cambia el nombre de los registros parciales. Pero no lo hacemos, ver más abajo ).

Resultados con un rendimiento de ~ 0.33c para las movsx r16, r8 (y movzx r16, r8 ):

Haswell resulta con un misterioso rendimiento de movsx/zx r16, r8 para movsx/zx r16, r8 :

Otros resultados anteriores y posteriores de Haswell (y CrystalWell) / Broadwell / Skylake tienen un rendimiento de 1.0c para esas dos pruebas.

  • HSW con 4.1.570.0 5 de junio de 2013, BDW con 4.3.15787.0 12 de octubre de 2018, BDW con 4.3.739.0 17 de marzo de 2017.

Como informé en el problema vinculado InstLat en github, los números de "latencia" para movzx r32, r8 ignoran la eliminación de mov, presumiblemente probando como movzx eax, al .

Peor aún, las versiones más nuevas de InstLatX64 con versiones de registros separados de la prueba, como MOVSX r1_32, r2_8 , muestran números de latencia por debajo de 1 ciclo, como 0.3c para ese MOV SX en Skylake. Esto es una tontería total; Lo probé solo para estar seguro.

La MOVSX r1_16, r2_8 muestra la latencia 1c, por lo que aparentemente solo miden la latencia de la dependencia (falsa) de salida . (Que no existe para salidas de 32 bits y más anchas).

¡Pero esa MOVSX r1_16, r2_8 midió la latencia 1c en Sandybridge ! Entonces, tal vez mi teoría estaba equivocada sobre lo que nos dice la prueba movsx r16, r8 .

En Ryzen (AIDA64 compilación 4.3.781.0 21 de febrero de 2018), que sabemos que no hace ningún cambio de nombre de registro parcial , los resultados no muestran el efecto de rendimiento 1c que esperaríamos si la prueba realmente estuviera escribiendo el mismo registro de 16 bits repetidamente. Tampoco lo encuentro en ninguna CPU AMD anterior, con versiones anteriores de InstLatX64, como K10 o la familia Bulldozer.

43 X86 :MOVSX r16, r8 L: 0.28ns= 1.0c T: 0.11ns= 0.40c 44 X86 :MOVSX r32, r8 L: 0.28ns= 1.0c T: 0.07ns= 0.25c 45 AMD64 :MOVSX r64, r8 L: 0.28ns= 1.0c T: 0.12ns= 0.43c 46 X86 :MOVSX r32, r16 L: 0.28ns= 1.0c T: 0.12ns= 0.43c 47 AMD64 :MOVSX r64, r16 L: 0.28ns= 1.0c T: 0.13ns= 0.45c 48 AMD64 :MOVSXD r64, r32 L: 0.28ns= 1.0c T: 0.13ns= 0.45c

IDK por qué el rendimiento no es 0.25 para todos ellos; Parece raro Esta podría ser una versión del efecto de rendimiento 0.58c Haswell. Los números MOVZX son los mismos, con un rendimiento de 0.25 para la versión sin prefijos que lee R8 y escribe un R32. ¿Tal vez hay un cuello de botella en buscar / decodificar para obtener instrucciones más grandes? Pero movsx r32, r16 es del mismo tamaño que movsx r32, r8 .

Sin embargo, las pruebas de registro separado muestran el mismo patrón que en Intel, con latencia 1c solo para la que tiene que fusionarse. MOVZX es lo mismo.

2252 X86 :MOVSX r1_16, r2_8 L: 0.28ns= 1.0c T: 0.08ns= 0.28c 2253 X86 :MOVSX r1_32, r2_8 L: 0.07ns= 0.3c T: 0.07ns= 0.25c 2254 AMD64 :MOVSX r1_64, r2_8 L: 0.07ns= 0.3c T: 0.07ns= 0.25c 2255 X86 :MOVSX r1_32, r2_16 L: 0.07ns= 0.3c T: 0.07ns= 0.25c

Los resultados de la excavadora también son bastante similares a esto, pero, por supuesto, un rendimiento menor.