Tamaño int_fast8_t frente a tamaño int_fast16_t en la plataforma x86-64
Ya aprendí que en la plataforma x86-64, usar cualquier registro de 64 bits necesitaría un REX
prefijo, y cualquier dirección de menos de 64 bits requeriría un prefijo de tamaño de dirección.
En x86-64 bits:
E3
rel8 esjrcxz
67 E3
rel8 esjecxz
67
es 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_t
8 bits cuando otro tipo de definición rápida es de 64 bits?
¿Tiene algo que ver con la alineación?
Respuestas
¿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_t
se definen en long
todas 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).
long
es 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 daddu
y, addu
por 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_popcount
cualquier otra operación que pueda requerir más trabajo con más bits (especialmente sin popcnt
soporte 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 fast
los tamaños fueran el tamaño mínimo, excepto tal vez fast16
32 bits, evitando prefijos de tamaño de operando y cosas de registro parcial.
fast
plantea 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_t
tipos 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 int
elementos int_fastxx_t
locales 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 fast
tipos 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_t
los 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_device
nú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 mov
o movsxd
(32 a 64 bits) o movzx
o 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 int
s 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, eax
para extender a cero la mitad inferior / shr rax, 32
. O simplemente add ebx, eax
usar 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í, movsxd
las 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_t
evita esas movsxd
instrucciones, a costa de ser peor en otros casos. Tal vez estoy descartando esto porque sé que debo evitarlo, por ejemplo, usarlo unsigned
cuando 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 movzx
instrucciones 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_t
será 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).
jrcxz
vs. jecxz
es un ejemplo extraño porque usa el prefijo de tamaño de 67h
dirección , en lugar de 66h
tamañ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/jz
en un solo uop.