c assembly x86 cpu-registers micro-optimization

¿Puede el MOV de x86 ser realmente "gratis"? ¿Por qué no puedo reproducir esto en absoluto?



assembly cpu-registers (2)

Sigo viendo que las personas afirman que la instrucción MOV puede ser gratuita en x86, debido al cambio de nombre del registro.

Por mi vida, no puedo verificar esto en un solo caso de prueba. Cada caso de prueba que intento lo desacredita.

Por ejemplo, aquí está el código que estoy compilando con Visual C ++:

#include <limits.h> #include <stdio.h> #include <time.h> int main(void) { unsigned int k, l, j; clock_t tstart = clock(); for (k = 0, j = 0, l = 0; j < UINT_MAX; ++j) { ++k; k = j; // <-- comment out this line to remove the MOV instruction l += j; } fprintf(stderr, "%d ms/n", (int)((clock() - tstart) * 1000 / CLOCKS_PER_SEC)); fflush(stderr); return (int)(k + j + l); }

Esto produce el siguiente código de ensamblaje para el bucle (siéntase libre de producir esto como desee; obviamente no necesita Visual C ++):

LOOP: add edi,esi mov ebx,esi inc esi cmp esi,FFFFFFFFh jc LOOP

Ahora ejecuto este programa varias veces, y observo una diferencia bastante consistente del 2% cuando se elimina la instrucción MOV:

Without MOV With MOV 1303 ms 1358 ms 1324 ms 1363 ms 1310 ms 1345 ms 1304 ms 1343 ms 1309 ms 1334 ms 1312 ms 1336 ms 1320 ms 1311 ms 1302 ms 1350 ms 1319 ms 1339 ms 1324 ms 1338 ms

Entonces, ¿qué da? ¿Por qué el MOV no es "gratis"? ¿Es este ciclo demasiado complicado para x86?
¿Hay un solo ejemplo por ahí que pueda demostrar que MOV es gratis como la gente dice?
Si es así, ¿qué es? Y si no, ¿por qué todos afirman que MOV es gratis?


Aquí hay dos pequeñas pruebas que creo que muestran de manera concluyente evidencia de eliminación de mov:

__loop1: add edx, 1 add edx, 1 add ecx, 1 jnc __loop1

versus

__loop2: mov eax, edx add eax, 1 mov edx, eax add edx, 1 add ecx, 1 jnc __loop2

Si mov agrega un ciclo a una cadena de dependencia, se esperaría que la segunda versión tome alrededor de 4 ciclos por iteración. En mi Haswell, ambos toman alrededor de 2 ciclos por iteración, lo que no puede suceder sin la eliminación de mov.


El rendimiento del bucle en la pregunta no depende de la latencia de MOV, o (en Haswell) del beneficio de no usar una unidad de ejecución.

El bucle sigue siendo solo 4 uops para que el front-end se emita en el núcleo fuera de servicio. ( mov todavía tiene que ser rastreado por el núcleo fuera de orden, incluso si no necesita una unidad de ejecución, pero cmp/jc fusiona en un solo uop).

Las CPU de Intel desde Core2 han tenido un ancho de problema de 4 uops por reloj, por lo que el mov no impide que se ejecute a (casi) un iter por reloj en Haswell. También funcionaría a una por hora en Ivybridge (con eliminación de mov), pero no en Sandybridge (sin eliminación de mov). En SnB, sería aproximadamente un iter por 1.333c ciclos, con un cuello de botella en el rendimiento de ALU porque el mov siempre necesitaría uno . (SnB / IvB tienen solo tres puertos ALU, mientras que Haswell tiene cuatro).

Tenga en cuenta que el manejo especial en la etapa de cambio de nombre ha sido algo para x87 FXCHG (intercambiar st0 con st0 ) durante mucho más tiempo que MOV. Agner Fog enumera FXCHG como latencia 0 en PPro / PII / PIII (núcleo P6 de primera generación).

El bucle en la pregunta tiene dos cadenas de dependencia entrelazadas (el add edi,esi depende del EDI y del contador de bucles ESI), lo que lo hace más sensible a la programación imperfecta. Una ralentización del 2% frente a la predicción teórica debido a instrucciones aparentemente no relacionadas no es inusual, y pequeñas variaciones en el orden de las instrucciones pueden hacer este tipo de diferencia. Para ejecutarse exactamente a 1c por iter, cada ciclo necesita ejecutar un INC y un ADD. Dado que todos los INC y ADD dependen de la iteración anterior, la ejecución fuera de orden no puede ponerse al día ejecutando dos en un solo ciclo. Peor aún, el ADD depende del INC en el ciclo anterior, que es lo que quise decir con "enclavamiento", por lo que perder un ciclo en la cadena de depósito INC también detiene la cadena de depósito ADD.

Además, las ramas tomadas de forma prevista solo pueden ejecutarse en el puerto 6, por lo que cualquier ciclo en el que el puerto 6 no ejecute un cmp / jc es un ciclo de rendimiento perdido . Esto sucede cada vez que un INC o ADD roba un ciclo en el puerto 6 en lugar de ejecutarse en los puertos 0, 1 o 5. IDK si este es el culpable, o si el problema es perder ciclos en las cadenas de depósito INC / ADD. algunos de los dos.

Agregar el MOV adicional no agrega ninguna presión en el puerto de ejecución, suponiendo que se elimine al 100%, pero evita que el front-end se ejecute por delante del núcleo de ejecución . (Solo 3 de los 4 uops en el bucle necesitan una unidad de ejecución, y su CPU Haswell puede ejecutar INC y ADD en cualquiera de sus 4 puertos ALU: 0, 1, 5 y 6. Por lo tanto, los cuellos de botella son:

  • El rendimiento máximo de front-end de 4 uops por reloj. (El bucle sin MOV es de solo 3 uops, por lo que el front-end puede ejecutarse por delante).
  • rendimiento de rama tomada de uno por reloj.
  • la cadena de dependencia que implica esi (latencia INC de 1 por reloj)
  • la cadena de dependencia que implica edi (ADD latencia de 1 por reloj, y también depende del INC de la iteración anterior)

Sin el MOV, el front-end puede emitir los tres uops del bucle a 4 por reloj hasta que el núcleo fuera de servicio esté lleno. (AFAICT, "desenrolla" pequeños bucles en el bucle-buffer (Loop Stream Detector: LSD), por lo que un bucle con ABC uops puede emitirse en un patrón ABCA BCAB CABC ... El contador de lsd.cycles_4_uops para lsd.cycles_4_uops confirma que en su mayoría emite en grupos de 4 cuando emite uops).

Las CPU de Intel asignan uops a los puertos a medida que se emiten en el núcleo fuera de servicio . La decisión se basa en contadores que rastrean cuántos uops para cada puerto ya están en el planificador (también conocido como Reservation Station, RS). Cuando hay muchos Uops en el RS esperando para ejecutarse, esto funciona bien y generalmente debería evitar programar INC o ADD en el puerto 6. Y supongo que también evita programar el INC y ADD de modo que se pierda tiempo de cualquiera de esas cadenas de dep. Pero si el RS está vacío o casi vacío, los contadores no impedirán que un ADD o INC robe un ciclo en el puerto 6.

Pensé que estaba en algo aquí, pero cualquier programación subóptima debería permitir que el front-end se ponga al día y mantenga el back-end lleno. No creo que debamos esperar que el front-end provoque suficientes burbujas en la tubería para explicar una caída del 2% por debajo del rendimiento máximo, ya que el pequeño bucle debería ejecutarse desde el búfer del bucle a un rendimiento muy constante de 4 por reloj. Tal vez hay algo más pasando.

Un verdadero ejemplo del beneficio de la eliminación de mov .

lea para construir un bucle que solo tiene un mov por reloj, creando una demostración perfecta donde la eliminación de MOV tiene éxito al 100%, o el 0% del tiempo con mov same,same para demostrar el cuello de botella de latencia que produce.

Dado que el dec/jnz macro-fusionado es parte de la cadena de dependencia que involucra el contador de bucles, la programación imperfecta no puede retrasarlo. Esto es diferente del caso en el que cmp/jc "se bifurca" de la cadena de dependencia de la ruta crítica en cada iteración.

_start: mov ecx, 2000000000 ; each iteration decrements by 2, so this is 1G iters align 16 ; really align 32 makes more sense in case the uop-cache comes into play, but alignment is actually irrelevant for loops that fit in the loop buffer. .loop: mov eax, ecx lea ecx, [rax-1] ; we vary these two instructions dec ecx ; dec/jnz macro-fuses into one uop in the decoders, on Intel jnz .loop .end: xor edi,edi ; edi=0 mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h syscall ; sys_exit_group(0)

En la familia Intel SnB, LEA con uno o dos componentes en el modo de direccionamiento se ejecuta con latencia 1c (consulte http://agner.org/optimize/ y otros enlaces en el wiki de etiquetas x86 ).

Construí y ejecuté esto como un binario estático en Linux, por lo que los contadores de rendimiento del espacio de usuario para todo el proceso miden solo el bucle con una carga general insignificante de inicio / apagado. (la perf stat es realmente fácil en comparación con poner consultas de contador de rendimiento en el programa en sí)

$ yasm -felf64 -Worphan-labels -gdwarf2 mov-elimination.asm && ld -o mov-elimination mov-elimination.o && objdump -Mintel -drwC mov-elimination && taskset -c 1 ocperf.py stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,uops_issued.any,uops_executed.thread -r2 ./mov-elimination Disassembly of section .text: 00000000004000b0 <_start>: 4000b0: b9 00 94 35 77 mov ecx,0x77359400 4000b5: 66 66 2e 0f 1f 84 00 00 00 00 00 data16 nop WORD PTR cs:[rax+rax*1+0x0] 00000000004000c0 <_start.loop>: 4000c0: 89 c8 mov eax,ecx 4000c2: 8d 48 ff lea ecx,[rax-0x1] 4000c5: ff c9 dec ecx 4000c7: 75 f7 jne 4000c0 <_start.loop> 00000000004000c9 <_start.end>: 4000c9: 31 ff xor edi,edi 4000cb: b8 e7 00 00 00 mov eax,0xe7 4000d0: 0f 05 syscall perf stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xb1,umask=0x1,name=uops_executed_thread/ -r2 ./mov-elimination Performance counter stats for ''./mov-elimination'' (2 runs): 513.242841 task-clock:u (msec) # 1.000 CPUs utilized ( +- 0.05% ) 0 context-switches:u # 0.000 K/sec 1 page-faults:u # 0.002 K/sec 2,000,111,934 cycles:u # 3.897 GHz ( +- 0.00% ) 4,000,000,161 instructions:u # 2.00 insn per cycle ( +- 0.00% ) 1,000,000,157 branches:u # 1948.396 M/sec ( +- 0.00% ) 3,000,058,589 uops_issued_any:u # 5845.300 M/sec ( +- 0.00% ) 2,000,037,900 uops_executed_thread:u # 3896.865 M/sec ( +- 0.00% ) 0.513402352 seconds time elapsed ( +- 0.05% )

Como se esperaba, el ciclo se ejecuta 1G veces ( branches ~ = 1 billón). Los ciclos "extra" de 111k más allá de 2G son gastos generales que también están presentes en las otras pruebas, incluida la que no tiene mov . No se debe a fallas ocasionales de la eliminación de mov, pero sí escala con el recuento de iteraciones, por lo que no se trata solo de una sobrecarga de inicio. Probablemente sea por interrupciones del temporizador, ya que el rendimiento de IIRC Linux no juega con los contadores de rendimiento mientras maneja las interrupciones, y simplemente les permite seguir contando. ( perf virtualiza los contadores de rendimiento del hardware para que pueda obtener recuentos por proceso incluso cuando un hilo migra a través de las CPU). Además, las interrupciones del temporizador en el núcleo lógico hermano que comparte el mismo núcleo físico perturbarán un poco las cosas.

El cuello de botella es la cadena de dependencia transportada por el bucle que involucra el contador del bucle. 2G ciclos para 1G iters son 2 relojes por iteración, o 1 reloj por decremento. El confirma que la longitud de la cadena dep es de 2 ciclos. Esto solo es posible si mov tiene latencia cero . (Sé que no prueba que no haya otro cuello de botella. Realmente solo prueba que la latencia es a lo sumo 2 ciclos, si no crees mi afirmación de que la latencia es el único cuello de botella. Hay un resource_stalls.any contador de rendimiento, pero no tiene muchas opciones para desglosar qué recurso microarquitectura se agotó).

El bucle tiene 3 uops de dominio fusionado: mov , lea y dec/jnz macro-fusionado . El conteo 3G uops_issued.any confirma que: Cuenta en el dominio fusionado, que es toda la tubería desde los decodificadores hasta el retiro, excepto el planificador (RS) y las unidades de ejecución. (los pares de instrucciones con fusibles macro permanecen como un solo UOP en todas partes. Es solo para la micro fusión de tiendas o la carga ALU + que 1 UOP de dominio fusionado en el ROB rastrea el progreso de dos Uops de dominio no fusionado).

2G uops_executed.thread (dominio sin fusionar) nos dice que todos los movimientos se eliminaron (es decir, se manejaron mediante la etapa de emisión / cambio de nombre y se colocaron en el ROB en un estado ya ejecutado). Todavía ocupan el ancho de banda de emisión / retirada, y el espacio en la caché uop, y el tamaño del código. Ocupan espacio en el ROB, limitando el tamaño de la ventana fuera de servicio. Una instrucción mov nunca es gratis. Hay muchos posibles cuellos de botella microarquitectónicos además de los puertos de latencia y ejecución, el más importante generalmente es la tasa de emisión de 4 en el front-end.

En las CPU Intel, tener latencia cero es a menudo un problema mayor que no necesitar una unidad de ejecución, especialmente en Haswell y más tarde donde hay 4 puertos ALU. (Pero solo 3 de ellos pueden manejar uops vectoriales, por lo que los movimientos vectoriales no eliminados serían un cuello de botella más fácilmente, especialmente en el código sin muchas cargas o tiendas que quiten el ancho de banda frontal (4 uops de dominio fusionado por reloj) lejos de los uops ALU Además, programar uops para unidades de ejecución no es perfecto (más bien, como el más antiguo listo primero), por lo que los uops que no están en la ruta crítica pueden robar ciclos de la ruta crítica).

Si ponemos un nop o un xor edx,edx en el bucle, también se emitirían pero no se ejecutarían en las CPU de la familia Intel SnB.

La eliminación de movimiento de latencia cero puede ser útil para la extensión cero de 32 a 64 bits, y para 8 a 64. ( movzx eax, bl se elimina, movzx eax, bx no ).

Sin mov-elimination

Todas las CPU actuales que admiten la eliminación de mov no lo admiten para mov same,same por lo tanto, elija diferentes registros para enteros de extensión cero de 32 a 64 bits, o vmovdqa xmm,xmm a vmovdqa xmm,xmm cero a YMM en un caso raro donde sea necesario (A menos que necesite el resultado en el registro en el que ya está. Rebotar a un registro diferente y viceversa normalmente es peor). Y en Intel, lo mismo se aplica para movzx eax,al por ejemplo. (AMD Ryzen no mueve mov-eliminado movzx.) Las tablas de instrucciones de Agner Fog muestran que mov siempre se elimina en Ryzen, pero supongo que quiere decir que no puede fallar entre dos reglas diferentes de la misma manera que en Intel.

Podemos usar esta limitación para crear un micro punto de referencia que lo derrote a propósito.

mov ecx, ecx # CPUs can''t eliminate mov same,same lea ecx, [rcx-1] dec ecx jnz .loop 3,000,320,972 cycles:u # 3.898 GHz ( +- 0.00% ) 4,000,000,238 instructions:u # 1.33 insn per cycle ( +- 0.00% ) 1,000,000,234 branches:u # 1299.225 M/sec ( +- 0.00% ) 3,000,084,446 uops_issued_any:u # 3897.783 M/sec ( +- 0.00% ) 3,000,058,661 uops_executed_thread:u # 3897.750 M/sec ( +- 0.00% )

Esto requiere ciclos 3G para iteraciones 1G, porque la longitud de la cadena de dependencia es ahora de 3 ciclos.

El conteo de UOP de dominio fusionado no cambió, aún 3G.

Lo que sí cambió es que ahora el conteo de UOP del dominio no fusionado es el mismo que el del dominio fusionado. Todos los uops necesitaban una unidad de ejecución; ninguna de las instrucciones mov se eliminó, por lo que todas agregaron latencia 1c a la cadena dep transportada en bucle.

(Cuando hay uops micro fusionados, como add eax, [rsi] , el recuento de uops_executed puede ser mayor que uops_issued . Pero no tenemos eso).

Sin el mov en absoluto:

lea ecx, [rcx-1] dec ecx jnz .loop 2,000,131,323 cycles:u # 3.896 GHz ( +- 0.00% ) 3,000,000,161 instructions:u # 1.50 insn per cycle 1,000,000,157 branches:u # 1947.876 M/sec 2,000,055,428 uops_issued_any:u # 3895.859 M/sec ( +- 0.00% ) 2,000,039,061 uops_executed_thread:u # 3895.828 M/sec ( +- 0.00% )

Ahora volvemos a la latencia de 2 ciclos para la cadena dep transportada en bucle.

Nada es eliminado.

Probé en un Skylake i7-6700k de 3.9GHz. Obtengo resultados idénticos en un Haswell i5-4210U (dentro de 40k de 1G recuentos) para todos los eventos de rendimiento. Eso es aproximadamente el mismo margen de error que volver a ejecutar en el mismo sistema.

Tenga en cuenta que si ejecuté perf como root 1 , y conté cycles lugar de cycles:u (solo espacio de usuario), medirá la frecuencia de la CPU como exactamente 3.900 GHz. (IDK por qué Linux solo obedece la configuración de bios para turbo máximo justo después del reinicio, pero luego cae a 3.9GHz si lo dejo inactivo durante un par de minutos. Asus Z170 Pro Gaming mobo, Arch Linux con kernel 4.10.11-1-ARCH Vi lo mismo con Ubuntu. Escribir balance_performance en cada uno de /sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference desde /etc/rc.local arregla, pero escribir balance_power hace que retroceda a 3.9GHz nuevamente más tarde.)

1: actualización: como una mejor alternativa para ejecutar sudo perf , configuro sysctl kernel.perf_event_paranoid = 0 en /etc/syctl.d/99-local.conf

Debería obtener los mismos resultados en AMD Ryzen, ya que puede eliminar el movimiento entero. La familia Bulldozer de AMD solo puede eliminar copias de registro xmm. (Según Agner Fog, las copias de registro ymm son una mitad baja eliminada y una operación ALU para la mitad alta).

Por ejemplo, AMD Bulldozer e Intel Ivybridge pueden mantener un rendimiento de 1 por reloj para

movaps xmm0, xmm1 movaps xmm2, xmm3 movaps xmm4, xmm5 dec jnz .loop

Pero Intel Sandybridge no puede eliminar movimientos, por lo que obstaculizaría 4 UUP de ALU para 3 puertos de ejecución. Si fuera pxor xmm0,xmm0 lugar de movaps, SnB también podría soportar una iteración por reloj. (Pero la familia Bulldozer no pudo hacerlo, porque la reducción a cero de la imagen aún necesita una unidad de ejecución en AMD, aunque es independiente del valor anterior del registro. Y la familia Bulldozer solo tiene un rendimiento de 0.5c para PXOR).

Limitaciones de la eliminación de mov

Dos instrucciones MOV dependientes seguidas exponen una diferencia entre Haswell y Skylake.

.loop: mov eax, ecx mov ecx, eax sub ecx, 2 jnz .loop

Haswell: variabilidad menor de ejecución a ejecución (1.746 a 1.749 c / iter), pero esto es típico:

1,749,102,925 cycles:u # 2.690 GHz 4,000,000,212 instructions:u # 2.29 insn per cycle 1,000,000,208 branches:u # 1538.062 M/sec 3,000,079,561 uops_issued_any:u # 4614.308 M/sec 1,746,698,502 uops_executed_core:u # 2686.531 M/sec 745,676,067 lsd_cycles_4_uops:u # 1146.896 M/sec

No se eliminan todas las instrucciones MOV: aproximadamente 0,75 de las 2 por iteración usaban un puerto de ejecución. Cada MOV que se ejecuta en lugar de ser eliminado agrega 1c de latencia a la cadena dep transportada en bucle, por lo que no es una coincidencia que uops_executed y los cycles sean muy similares. Todos los uops son parte de una sola cadena de dependencia, por lo que no hay paralelismo posible. cycles siempre son aproximadamente 5M más altos que uops_executed independientemente de la uops_executed de ejecución a ejecución, por lo que supongo que solo se están utilizando 5M ciclos en otro lugar.

Skylake: más estable que los resultados de HSW y más eliminación de movimientos: solo 0.6666 MOV de cada 2 necesitaban una unidad de ejecución.

1,666,716,605 cycles:u # 3.897 GHz 4,000,000,136 instructions:u # 2.40 insn per cycle 1,000,000,132 branches:u # 2338.050 M/sec 3,000,059,008 uops_issued_any:u # 7014.288 M/sec 1,666,548,206 uops_executed_thread:u # 3896.473 M/sec 666,683,358 lsd_cycles_4_uops:u # 1558.739 M/sec

En Haswell, lsd.cycles_4_uops representaba todos los uops. (0.745 * 4 ~ = 3). Entonces, en casi todos los ciclos donde se emiten uops, se emite un grupo completo de 4 (desde el buffer de bucle. Probablemente debería haber mirado un contador diferente que no le importa de dónde vinieron, como uops_issued.stall_cycles para contar ciclos donde no se emitieron uops).

Pero en SKL, 0.66666 * 4 = 2.66664 es inferior a 3, por lo que en algunos ciclos el front-end emitió menos de 4 uops. (Por lo general, se detiene hasta que haya espacio en el núcleo fuera de servicio para emitir un grupo completo de 4, en lugar de emitir grupos no completos).

Es extraño, IDK, cuál es la limitación microarquitectura exacta. Como el ciclo es de solo 3 uops, cada grupo de problemas de 4 uops es más que una iteración completa. Por lo tanto, un grupo de problemas puede contener hasta 3 MOV dependientes. ¿Quizás Skylake está diseñado para romper eso a veces, para permitir una mayor eliminación de movimientos?

actualización : en realidad esto es normal para los bucles de 3 uop en Skylake. uops_issued.stall_cycles muestra que HSW y SKL emiten un simple bucle de 3 uop sin eliminación de movimiento de la misma manera que emiten este. Por lo tanto, una mejor eliminación de mov es un efecto secundario de la división de grupos de problemas por alguna otra razón. (No es un cuello de botella porque las ramas tomadas no pueden ejecutarse más rápido que 1 por reloj, independientemente de lo rápido que se emitan). Todavía no sé por qué SKL es diferente, pero no creo que sea algo de qué preocuparse.

En un caso menos extremo, SKL y HSW son iguales, y ambos no logran eliminar 0.3333 de cada 2 instrucciones MOV:

.loop: mov eax, ecx dec eax mov ecx, eax sub ecx, 1 jnz .loop

2,333,434,710 cycles:u # 3.897 GHz 5,000,000,185 instructions:u # 2.14 insn per cycle 1,000,000,181 branches:u # 1669.905 M/sec 4,000,061,152 uops_issued_any:u # 6679.720 M/sec 2,333,374,781 uops_executed_thread:u # 3896.513 M/sec 1,000,000,942 lsd_cycles_4_uops:u # 1669.906 M/sec

Todos los uops se emiten en grupos de 4. Cualquier grupo contiguo de 4 uops contendrá exactamente dos MOV uops que son candidatos para la eliminación. Como claramente logra eliminar ambos en algunos ciclos, IDK explica por qué no siempre puede hacer eso.

El manual de optimización de Intel dice que sobrescribir el resultado de la eliminación de mov lo antes posible libera los recursos de microarquitectura para que pueda tener éxito con mayor frecuencia, al menos para movzx . Ver Ejemplo 3-25. Secuencia de reordenamiento para mejorar la efectividad de las instrucciones MOV de latencia cero .

Entonces, ¿tal vez se realiza un seguimiento interno con una tabla de recuentos de tamaño limitado? Algo tiene que evitar que la entrada del archivo de registro físico se libere cuando ya no se necesita como el valor del registro arquitectónico original, si todavía se necesita como el valor del destino mov. La liberación de entradas PRF lo antes posible es clave, ya que el tamaño PRF puede limitar la ventana fuera de servicio a un tamaño menor que el tamaño ROB.

Probé los ejemplos en Haswell y Skylake, y descubrí que la eliminación de movimiento funcionaba significativamente más tiempo al hacerlo, pero que en realidad era un poco más lenta en ciclos totales, en lugar de más rápido. El ejemplo tenía la intención de mostrar el beneficio en IvyBridge, que probablemente tiene cuellos de botella en sus 3 puertos ALU, pero HSW / SKL solo tiene cuellos de botella en conflictos de recursos en las cadenas de dep y no parece molestarse al necesitar un puerto ALU para más de instrucciones movzx .

Consulte también ¿Por qué XCHG reg, reg es una instrucción 3 micro-op en arquitecturas Intel modernas? para más investigación + conjeturas sobre cómo funciona la eliminación de mov, y si podría funcionar para xchg eax, ecx . (En la práctica, xchg reg,reg es 3 ALU uops en Intel, pero 2 eliminó uops en Ryzen. Es interesante adivinar si Intel podría haberlo implementado de manera más eficiente).

Por cierto, como solución para una errata en Haswell, Linux no proporciona uops_executed.thread cuando hyperthreading está habilitado, solo uops_executed.core . El otro núcleo definitivamente estuvo inactivo todo el tiempo, ni siquiera las interrupciones del temporizador, porque lo desconecté con echo 0 > /sys/devices/system/cpu/cpu3/online . Desafortunadamente, esto no se puede hacer antes de que perf decida que HT está habilitado, y mi computadora portátil Dell no tiene una opción de BIOS para deshabilitar HT. Por lo tanto, no puedo obtener perf para usar los 8 contadores de PMU de hardware a la vez en ese sistema, solo 4.: /