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

7 minutos de lectura

avatar de usuario
usuario541686

Sigo viendo que la gente afirma 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 ensamblador para el ciclo (siéntete libre de producirlo como quieras; obviamente no necesitas 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 bucle demasiado complicado para x86?
Hay un soltero ejemplo por ahí que puede demostrar que MOV es libre como la gente afirma?
Si es así, ¿qué es? Y si no, ¿por qué todos siguen afirmando que MOV es gratis?

  • La “gratuidad” tiene que ver con la latencia, que no está midiendo aquí. Además, el 2% de eso es significativamente menos que un ciclo, por lo que solo se debe a “efectos extraños”

    – harold

    24 mayo 2017 a las 22:19

  • Bueno, ¿qué significa “totalmente eliminado”? Claramente, no se puede eliminar antes de decodificar, porque aún no se sabe qué es. Como era de esperar, el truco de cambio de nombre puede, en el mejor de los casos, eliminar el mov durante el cambio de nombre y luego ni siquiera siempre. Solo por estar ahí, el mov no puede ser enteramente gratis.

    – harold

    24 mayo 2017 a las 22:35

  • Agregaste un 25 % más de instrucciones, pero es solo un 2 % más lento. No puede explicar eso con “parece que no hay eliminación de MOV”. Una diferencia del 2% requiere otra explicación, como que el núcleo se calienta demasiado y se ralentiza.

    -Hans Passant

    24 mayo 2017 a las 22:59

  • El cambio de nombre de registro elimina efectivamente el MOV del back-end, lo que significa que consta de 0 µops, no consume un puerto de ejecución y tiene 0 latencia. Sin embargo, la instrucción en sí todavía tiene que ser decodificada, lo cual no es gratis. Además, ocupa espacio en el código, lo que significa espacio en el caché. Así que no, un MOV nunca es realmente gratis, porque hay costos en el front-end, pero a menudo es efectivamente free en el contexto de un bloque de código más grande que está realizando alguna operación significativa. Una diferencia del 2% en la velocidad de ejecución es claramente lejos menos de un ciclo, como cabría esperar ingenuamente.

    – Cody gris

    26 de mayo de 2017 a las 9:44


  • @CodyGray: un MOV eliminado ocupa espacio en el ROB hasta que se retira (lo mismo que una instrucción xor-zeroing o incluso un NOP), en hardware Intel (sin predicciones erróneas de rama, uops_retired.retire_slots coincidirá casi exactamente uops_issued.any). Mi modelo mental es que ingresan al ROB (dominio fusionado) en un estado listo para retirar ya ejecutado, con cero uops de dominio no fusionado emitidos en el RS (programador). Presumiblemente, hay algo no trivial en no tener una opción para retirarse para recibir una instrucción, tal vez algo en actualizar RIP o simplemente revertir las especulaciones erróneas…

    – Peter Cordes

    28 de mayo de 2017 a las 3:59


  • +1 gran respuesta! Algo de esto realmente se me pasó por la cabeza (por ejemplo, no había oído hablar antes de “dominio fusionado”), pero creo que entendí lo que estaba pasando. ¡Gracias!

    – usuario541686

    26 de mayo de 2017 a las 10:18

  • Sí, estoy bastante seguro de que lo entiendo. Está diciendo que dec + jnz se fusionan en 1 operación, por lo que si se elimina el mov, tiene 2 operaciones ejecutándose cada una para 4 instrucciones, y cada una toma un ciclo, dando 2.00 ins/ciclo, y análogamente con 1.33 y 1,50 casos. El 2% es definitivamente curioso, estoy de acuerdo. Pero es una muy buena respuesta; Iba a aceptarlo en algún momento, simplemente no tenía prisa por ello. Gracias por escribirlo.

    – usuario541686

    26 mayo 2017 a las 23:39

  • @JDługosz: movzx eax, bl es de 8 a 64. La parte 32 -> 64 está implícita al escribir un registro de 32 bits (stackoverflow.com/questions/11177137/…). Escritura movzx rax, bl haría el código más grande (prefijo REX) sin ningún beneficio.

    – Peter Cordes

    19 de septiembre de 2017 a las 7:34


  • @BeeOnRope: Oh, FFS Intel, pruebe mejor sus CPU para que no tengamos que seguir trabajando en los baches de rendimiento introducidos por las mitigaciones. Sobre todo porque el consejo de optimización de Intel para IvyBridge era preferir sobrescribir el resultado de una mov de inmediato para liberar recursos de eliminación de movimiento, lo que hace más probable que el mov estar en el camino crítico sin eliminación. (Y los compiladores parecen preferir hacer más con la copia en lugar del original después de hacer una copia).

    – Peter Cordes

    10 de marzo de 2021 a las 23:18

  • @Noah: Lástima que el microcódigo de Intel no sea de código abierto; sabemos que el LSD se puede desactivar mediante un microcódigo, como en la familia Skylake. (Por supuesto, si tuviera varias computadoras para elegir, podría usar un SKL con su LSD deshabilitado por microcódigo, en lugar de uno que no lo hiciera, suponiendo que, por lo demás, son microarquitectónicamente idénticos).

    – Peter Cordes

    28 de abril de 2021 a las 0:57

  • @Mehrdad porque el movLos correos electrónicos ahora están en la cadena de dependencia, por lo que si tuvieran una latencia, tendría que sumarse. En su caso de prueba, el mov está colgando al final de la cadena, nada está esperando a que suceda. Puede ser eliminado o no, no hay forma de saberlo.

    – harold

    24 mayo 2017 a las 23:00


  • @Mehrdad los tiempos son diferentes, sí. Pero la latencia solo puede ser (inb4 Netburst con su extraña ALU de doble bomba) ser un número entero de ciclos, por lo que mov o agrega un ciclo o no lo hace (en cuyo caso debe haber sido eliminado). Que su sola presencia tiene otro efectos (más sutiles), en realidad no tiene relación. Tienes toda la razón, por supuesto, en que esos efectos existen.

    – harold

    24 mayo 2017 a las 23:11


  • @Mehrdad eso se está metiendo un poco en casos extraños ya que depende de cómo se implemente, al menos es posible tratar para medirlo ya que teóricamente lee algo y escribe algo. En realidad, hacer eso (por ejemplo, adaptando el código de mi segundo caso de prueba) muestra que su latencia es 1 en Haswell (es decir, no se elimina). No puedo pensar en una razón para eso fuera de mi cabeza, pero así es como es.

    – harold

    24 mayo 2017 a las 23:39

  • @Mehrdad, oh, lo siento, sí, una latencia promedio puede ser un número no entero. Bajo la hipótesis de que lo que está sucediendo es ocasional Si no se elimina el mov, incluso se podría decir que la latencia es, en promedio, un número bajo pero distinto de cero. AFAIK, solo se debe a otros efectos, pero siempre vale la pena intentarlo. E: por ejemplo, si la pequeña penalización constante para mi segundo ejemplo cambia significativamente si se coloca “otra basura inofensiva” en lugar de movs, eso podría indicar algo interesante en esa dirección.

    – harold

    24 mayo 2017 a las 23:50


  • ¿Estás ejecutando este baremetal? con o sin cachés habilitados? ¿Ajusta la alineación de búsqueda a través de al menos 16 si no 32 bytes?

    – viejo contador de tiempo

    25 de mayo de 2017 a las 2:55

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad