Tamaño int_fast8_t frente a tamaño int_fast16_t en la plataforma x86-64

Oct 01 2023

Ya aprendí que en la plataforma x86-64, usar cualquier registro de 64 bits necesitaría un REXprefijo, y cualquier dirección de menos de 64 bits requeriría un prefijo de tamaño de dirección.

En x86-64 bits:

E3rel8 esjrcxz

67 E3rel8 esjecxz

67es un código de operación para el prefijo de anulación del tamaño de la dirección.

sizeof(int_fast8_t)es de 8 bits, mientras que otros sizeof(int_fast16_t)y sizeof(int_fast32_t)(solo en Linux) son de 64 bits.

¿Por qué solo son int_fast8_t8 bits cuando otro tipo de definición rápida es de 64 bits?

¿Tiene algo que ver con la alineación?

Respuestas

13 PeterCordes Oct 01 2023 at 21:21

¿Por qué solo int_fast8_t es de 8 bits mientras que otros typdef rápidos son de 64 bits?

Porque glibc tomó una decisión simplista y posiblemente mala cuando x86-64 era nuevo cuando estos tipos C99 eran nuevos, y tomó la mala decisión de no especializarlo para x86-64.

Todos int_fast16/32/64_tse definen en longtodas las plataformas. Esto se hizo en mayo de 1999 antes de que se anunciara AMD64 con una especificación en papel (octubre de 1999) que presumiblemente los desarrolladores tardaron algún tiempo en asimilar. (Gracias @ Homer512 por encontrar el compromiso y el historial).

longes el ancho de registro completo (entero) en sistemas GNU de 32 y 64 bits. Que también es el ancho del puntero.

Para la mayoría de los RISC de 64 bits, el ancho completo es bastante natural, aunque IDK habla de velocidades de multiplicación y división. Es notoriamente malo para x86-64, donde el tamaño de operando de 64 bits requiere un tamaño de código adicional, pero MIPS dadduy, addupor ejemplo, tienen el mismo tamaño de código y presumiblemente un rendimiento equivalente. (Antes de x86-64, era común que las ABI de RISC mantuvieran el signo de tipos estrechos , extendido a 64 bits todo el tiempo, porque MIPS al menos en realidad lo requería para instrucciones sin desplazamiento. Consulte MOVZX falta registro de 32 bits a registro de 64 bits para un poco más de historia.)

La elección de Glibc hace que estos tipos sean en su mayoría adecuados para variables locales, al menos si no multiplica ni divide o __builtin_popcountcualquier otra operación que pueda requerir más trabajo con más bits (especialmente sin popcntsoporte de hardware). Pero no es bueno en ningún lugar donde el espacio de almacenamiento en la memoria importe.

Si esperaba un tipo "elija un tamaño mayor al especificado solo si eso evita problemas de rendimiento", eso no es ni remotamente lo que le ofrece glibc.


Me parece recordar que MUSL hizo una mejor elección en x86-64, como si tal vez todos fastlos tamaños fueran el tamaño mínimo, excepto tal vez fast1632 bits, evitando prefijos de tamaño de operando y cosas de registro parcial.

fastplantea la pregunta "¿rápido para qué?", ​​y la respuesta no es el mismo tamaño para todos los casos de uso . Por ejemplo, en algo que puede vectorizarse automáticamente con SIMD, los números enteros más estrechos posibles suelen ser los mejores, para realizar el doble de trabajo por instrucción vectorial de 16 bytes. En ese caso, se pueden justificar los números enteros de 16 bits. O simplemente para la huella de caché en matrices. Pero no espere que los fastxx_ttipos consideren una compensación entre "no mucho más lento" versus ahorrar tamaño en matrices.

Por lo general, las instrucciones de carga/almacenamiento estrechas están bien en la mayoría de las ISA, por lo que debe tener intelementos int_fastxx_tlocales y de matriz estrecha si el espacio de caché es una consideración relevante. Pero la elección de glibc suele ser mala incluso para las variables locales.


Tal vez la gente de glibc solo contaba instrucciones, no el tamaño del código (prefijos REX) o el costo de multiplicar y dividir (que definitivamente era más lento para 64 bits que para 32 o más estrecho, especialmente en las primeras CPU AMD64; la división de enteros aún era mucho más lenta). para 64 bits en Intel hasta Ice Lakehttps://uops.info/yhttps://agner.org/optimize/).

Y sin mirar el efecto en el tamaño de las estructuras tanto directamente como debido a alignof(T) == 8. (Aunque los tamaños de los fasttipos no están establecidos en la ABI del System V x86-64, probablemente sea mejor no usarlos en los límites de la ABI, como estructuras involucradas en una API de biblioteca).

Realmente no sé por qué cometieron un error tan grave, pero hace que int_fastxx_tlos tipos sean inútiles para cualquier cosa excepto para variables locales (no para la mayoría de estructuras o matrices) porque x86-64 GNU/Linux es una plataforma importante para la mayoría del código portátil, y no No quiero que tu código apeste allí.

Algo así como la decisión de MinGW de devolver std::random_devicenúmeros aleatorios de baja calidad (en lugar de fallar hasta que lograron implementar algo utilizable) fue como arrojar desechos radiactivos en él en cuanto a que el código portátil pueda usar la función de lenguaje para el propósito previsto. objetivo.


Una de las pocas ventajas de usar enteros de 64 bits es tal vez evitar lidiar con basura en la parte alta de los registros en los límites de ABI (argumentos de función y valores de retorno). Pero normalmente eso no importa, a menos que necesite extenderlo al ancho del puntero como parte de un modo de direccionamiento. (En x86-64, todos los registros en un modo de direccionamiento deben tener el mismo ancho, como [rdi + rdx*4]. AArch64 tiene modos como [x0, w1 sxt]ese: extiende un registro de 32 bits como índice para un registro de 64 bits. Pero el formato de código de máquina de AArch64 fue diseñado desde cero y llegó más tarde con la visión retrospectiva de ver otros ISA de 64 bits en acción).

por ejemplo, arr[ foo(i) ]puede evitar una instrucción para extender a cero un valor de retorno si el tipo de retorno llena un registro. De lo contrario, es necesario extenderlo con signo o cero al ancho del puntero antes de poder usarlo en modo de direccionamiento, con movo movsxd(32 a 64 bits) o movzxo movsx(8 o 16 bits a 64 bits).

O con la forma en que x86-64 System V pasa y devuelve estructuras por valor en hasta 2 registros, los enteros de 64 bits no necesitan descomprimirse porque ya están en un registro por sí mismos. por ejemplo, struct ( int32_t a,b; }tiene ambos ints empaquetados en RAX en un valor de retorno, necesitando trabajo en el destinatario para empaquetar y en la persona que llama para descomprimir si realmente usa el resultado, no solo almacena la representación del objeto en una estructura en la memoria. (por ejemplo, mov ecx, eaxpara extender a cero la mitad inferior / shr rax, 32. O simplemente add ebx, eaxusar la mitad inferior y luego descartarla con el desplazamiento; no es necesario extenderla a cero a 64 bits para usarla solo como un entero de 32 bits .)

Dentro de una función, los compiladores sabrán que un valor ya está extendido a cero a 64 bits después de escribir un registro de 32 bits. Y al cargar desde la memoria, incluso la extensión de señal a 64 bits es gratuita ( movsxd rax, [rdi]en lugar de mov eax, [rdi]). (O casi gratis en CPU más antiguas donde la extensión de signo de fuente de memoria todavía necesitaba una ALU uop, no se realiza como parte de una carga uop).

Debido a que el desbordamiento de enteros con signo es UB, los compiladores pueden ampliar int( int32_t) a 64 bits en bucles como for (int i = 0 ; i < n ; i++ ) arr[i] += 1;o convertirlo en un incremento de puntero de 64 bits. (Me pregunto si GCC tal vez no podría hacer esto a principios de la década de 2000, cuando se tomaban estas decisiones de diseño de software. En ese caso, sí, movsxdlas instrucciones desperdiciadas para seguir extendiendo un contador de bucle a 64 bits serían una consideración interesante. .)

Pero para ser justos, aún puede tener instrucciones de extensión de signo al usar tipos enteros de 32 bits con signo en los cálculos, lo que podría producir resultados negativos si luego los usa para indexar matrices. Entonces 64 bits int_fast32_tevita esas movsxdinstrucciones, a costa de ser peor en otros casos. Tal vez estoy descartando esto porque sé que debo evitarlo, por ejemplo, usarlo unsignedcuando sea apropiado porque sé que se extiende cero de forma gratuita en x86-64 y AArch64.


Para el cálculo real, el tamaño de operando de 32 bits es generalmente al menos tan rápido como cualquier otra cosa, incluso para imul/div y popcnt , y evita penalizaciones de registro parcial o movzxinstrucciones adicionales que se obtienen con 8 o 16 bits.

  • Las ventajas de utilizar registros/instrucciones de 32 bits en x86-64
  • ¿Por qué el tamaño de operando predeterminado es de 32 bits en modo 64? - 32 bits no necesita REX ni prefijo de tamaño de operando.

Pero 8 bits no está mal, y si sus números son tan pequeños, es aún peor aumentarlos a 32 o 64 bits; Probablemente haya más expectativas por parte de los programadores de que int_fast8_tserá pequeño a menos que sea mucho más costoso hacerlo más grande. No está en x86-64; ¿Existen CPU modernas en las que un almacén de bytes en caché sea en realidad más lento que un almacén de palabras? - Sí, aparentemente la mayoría no es x86, pero x86 hace que los bytes y las palabras de 16 bits sean rápidos para la carga/almacenamiento, así como para el cálculo.

Evitar los 16 bits probablemente sea bueno; en algunos casos, vale la pena el costo de 2 bytes adicionales. add ax, 12345(y otras instrucciones imm16) tienen paradas de decodificación LCP en CPU Intel. Además de dependencias falsas de registro parcial (o en CPU más antiguas, bloqueos de fusión).


jrcxzvs. jecxzes un ejemplo extraño porque usa el prefijo de tamaño de 67h dirección , en lugar de 66htamaño de operando. Y porque los compiladores nunca (?) lo usan. No es tan lento como la loopinstrucción , pero sorprendentemente no es un uop único, ni siquiera en las CPU Intel que pueden fusionar macro a test/jzen un solo uop.