¿Por qué la variable corta (16 bits) mueve un valor a un registro y lo almacena, a diferencia de otros anchos?

int main()
{
00211000  push        ebp  
00211001  mov         ebp,esp  
00211003  sub         esp,10h  
    char charVar1;
    short shortVar1;
    int intVar1;
    long longVar1;
    
    charVar1 = 11;
00211006  mov         byte ptr [charVar1],0Bh  

    shortVar1 = 11;
0021100A  mov         eax,0Bh  
0021100F  mov         word ptr [shortVar1],ax  

    intVar1 = 11;
00211013  mov         dword ptr [intVar1],0Bh 
 
    longVar1 = 11;
0021101A  mov         dword ptr [longVar1],0Bh  
}

Otros tipos de datos no pasan por registros, pero solo los tipos cortos pasan por registros. ¿Qué ocurre?

Answer

GCC hace lo mismo, usando mov reg, imm32/ mov m16, regen lugar de mov mem, imm16.

Es para evitar que LCP se detenga en las CPU de la familia Intel P6 con un tamaño de operando de 16 bits mov imm16.

Se produce una parada de LCP (prefijo de cambio de longitud) cuando un prefijo cambia la longitud del resto de la instrucción en comparación con los mismos bytes de código de máquina sin prefijos.

mov word ptr [ebp - 8], 11implicaría un 66prefijo que hace que el resto de la instrucción sea de 5 bytes (opcode + modrm + disp8 + imm16) en lugar de 7 (opcode + modrm + disp8 + imm32) para el mismo código de operación/modrm.)

 66 c7 45 f8 0b 00          mov     WORD PTR [ebp-0x8],0xb
    c7 45 f8 0b 00 00 00    mov    DWORD PTR [ebp-0x8],0xb
    ^
  opcode

Este cambio de longitud confunde la etapa de búsqueda de la longitud de la instrucción (predecodificación) que ocurre antes de que los fragmentos de código de máquina se enruten a los decodificadores reales. Se ven obligados a retroceder y utilizar un método más lento que tiene en cuenta los prefijos en la forma en que miran los códigos de operación. (La decodificación paralela del código de máquina x86 es difícil). La penalización por esta copia de seguridad puede ser de hasta 11 ciclos, según la microarquitectura y la alineación de la instrucción, y debe evitarse en la medida de lo posible.

Consulte ¿Se bloquea un prefijo de cambio de longitud (LCP) en una instrucción x86_64 simple? para obtener muchos detalles sobre qué es un bloqueo de prefijo de cambio de longitud, y el efecto de rendimiento de detener la etapa de decodificación previa en Intel P6 y CPU de la familia SnB durante algunos ciclos , y los casos especiales de la familia Sandybridge (Intel convencional moderno). movcódigos de operación para evitar paradas de LCP de inmediatos de 16 bits.


mov específicamente no tiene un problema en Intel moderno

La familia Sandybridge eliminó las paradas de LCP movespecíficamente (todavía existe para otras instrucciones), por lo que esta decisión de ajuste solo ayuda a Nehalem y antes.

AFAIK, no es algo en la familia Silvermont, ni en ningún AMD, por lo que probablemente sea algo que MSVC y GCC deberían actualizar, tune=genericya que las CPU de la familia P6 son cada vez menos relevantes en estos días. (Y si las últimas versiones de desarrollo de GCC / MSVC cambiaran ahora, pasaría otro año más o menos antes de que se crearan muchas distribuciones / lanzamientos de software con un nuevo compilador).

clangno hace esta optimización, y no es un desastre incluso en las antiguas CPU de la familia P6 porque la mayoría del software no usa muchas variables short/ int16_t. (Y el cuello de botella no siempre es el front-end, a menudo falla el caché).


Ejemplos

El almacenamiento en la pila para esta función se debe, por supuesto, a que no se habilita la optimización. Dado que esas variables no son volatile, deben optimizarse por completo, ya que nada las lee más tarde. Cuando desee hacer ejemplos de salida de ASM, no escriba un main, escriba una función que tenga algún efecto secundario, por ejemplo, almacenamiento a través de un puntero, o use volatile.

void foo(short *p){
    volatile short x = 123;
    *p = 123;
}

Se compila con MSVC 19.14 -O2( https://godbolt.org/z/eWhzhEsEa ):

x$ = 8
p$ = 8
foo     PROC                                          ; COMDAT
        mov     eax, 123                      ; 0000007bH
        mov     WORD PTR x$[rsp], ax
        mov     WORD PTR [rcx], ax
        ret     0
foo     ENDP

O con GCC11.2 -O3, que apesta aún más, no CSEing /reutilización de la constante de registro

foo:
        mov     eax, 123
        mov     edx, 123
        mov     WORD PTR [rsp-2], ax
        mov     WORD PTR [rdi], dx
        ret

Pero podemos ver que se trata de un tuning de Intel ya que con -O3 -march=znver1(AMD Zen 1):

foo:
        mov     WORD PTR [rsp-2], 123
        mov     WORD PTR [rdi], 123
        ret

Desafortunadamente, todavía evita LCP movcon -march=skylake, por lo que no conoce las reglas completas.

Y si usamos *p += 12345;(un número lo suficientemente grande como para no caber en un imm8, que agregar permite a diferencia de mov) en lugar de solo =, irónicamente, GCC usa un prefijo de cambio de longitud con -march=skylake(al igual que MSVC), creando un estancamiento: add WORD PTR [rdi], 12345.