gcc assembly x86 x86-64

¿Por qué GCC no utiliza registros parciales?



assembly x86 (3)

De hecho, gcc muy a menudo usa registros parciales . Si observa el código generado, encontrará muchos casos en los que se utilizan registros parciales.

La respuesta corta para su caso particular es que gcc siempre firma o extiende los argumentos a cero a 32 bits cuando llama a una función C ABI .

El SysV x86 y x86-64 ABI de facto adoptado por gcc y clang requiere que los parámetros más pequeños que 32 bits sean cero o con signo extendido a 32 bits. Curiosamente, no necesitan extenderse hasta 64 bits.

Entonces, para una función como la siguiente en una plataforma SysV ABI de plataforma de 64 bits:

void foo(short s) { ... }

... el argumento s se pasa en rdi y los bits de s serán los siguientes (pero vea mi advertencia a continuación con respecto a icc ):

bits 0-31: SSSSSSSS SSSSSSSS SPPPPPPP PPPPPPPP bits 32-63: XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX where: P: the bottom 15 bits of the value of `s` S: the sign bit of `s` (extended into bits 16-31) X: arbitrary garbage

El código para foo puede depender de los bits S y P , pero no de los bits X , que pueden ser cualquier cosa.

Del mismo modo, para foo_unsigned(unsigned short u) , tendría 0 en los bits 16-31, pero de lo contrario sería idéntico.

Tenga en cuenta que dije de facto , porque en realidad no está realmente documentado qué hacer para los tipos de retorno más pequeños, pero puede ver la respuesta de Peter aquí para más detalles. También hice una pregunta relacionada here .

Después de algunas pruebas adicionales, concluí que icc realidad rompe este estándar de facto. gcc y clang parecen adherirse a él, pero gcc solo de manera conservadora: cuando llama a una función, hace argumentos de extensión cero / signo a 32 bits, pero en sus implementaciones de función no depende de la persona que realiza la llamada . clang implementa funciones que dependen de que el llamante extienda los parámetros a 32 bits. De hecho, clang e icc son mutuamente incompatibles incluso para las funciones simples de C si tienen parámetros más pequeños que int .

Desmontando write(1,"hi",3) en linux, construido con gcc -s -nostdlib -nostartfiles -O3 resulta en:

ba03000000 mov edx, 3 ; thanks for the correction jester! bf01000000 mov edi, 1 31c0 xor eax, eax e9d8ffffff jmp loc.imp.write

No estoy en el desarrollo del compilador, pero dado que cada valor movido a estos registros es constante y conocido en tiempo de compilación, tengo curiosidad por qué gcc no usa dl , dil y al lugar. Algunos pueden argumentar que esta característica no hará ninguna diferencia en el rendimiento, pero hay una gran diferencia en el tamaño ejecutable entre mov $1, %rax => b801000000 y mov $1, %al => b001 cuando estamos hablando de miles de accesos de registro en un programa. No solo el tamaño pequeño es parte de la elegancia de un software, sino que tiene efecto en el rendimiento.

¿Alguien puede explicar por qué "GCC decidió" que no importa?


En algo como la PC IBM original, si se sabía que AH contenía 0 y era necesario cargar AX con un valor como 0x34, usar "MOV AL, 34h" generalmente tomaría 8 ciclos en lugar de los 12 requeridos para "MOV AX, 0034h ": una mejora de velocidad bastante grande (cualquiera de las instrucciones podría ejecutarse en 2 ciclos si se busca previamente, pero en la práctica el 8088 pasa la mayor parte del tiempo esperando que las instrucciones se obtengan a un costo de cuatro ciclos por byte). Sin embargo, en los procesadores utilizados en las computadoras de uso general de hoy en día, el tiempo requerido para obtener el código generalmente no es un factor significativo en la velocidad general de ejecución, y el tamaño del código normalmente no es una preocupación particular.

Además, los proveedores de procesadores intentan maximizar el rendimiento de los tipos de código que las personas pueden ejecutar, y las instrucciones de carga de 8 bits no se usan con tanta frecuencia hoy en día como las instrucciones de carga de 32 bits. Los núcleos de procesador a menudo incluyen lógica para ejecutar múltiples instrucciones de 32 bits o 64 bits simultáneamente, pero pueden no incluir lógica para ejecutar una operación de 8 bits simultáneamente con cualquier otra cosa. En consecuencia, si bien utilizar operaciones de 8 bits en el 8088 cuando fue posible fue una optimización útil en el 8088, en realidad puede ser una pérdida significativa de rendimiento en los procesadores más nuevos.


Los registros parciales implican una penalización de rendimiento en muchos procesadores x86 porque se renombran en diferentes registros físicos de toda su contraparte cuando se escriben. (Para obtener más información sobre el cambio de nombre del registro que permite la ejecución fuera de orden, consulte estas preguntas y respuestas ).

Pero cuando una instrucción lee todo el registro, la CPU tiene que detectar el hecho de que no tiene el valor de registro arquitectónico disponible en un solo registro físico. (Esto sucede en la etapa de emisión / cambio de nombre, ya que la CPU se prepara para enviar el uop al planificador fuera de servicio).

Se llama un puesto de registro parcial . El manual de microarquitectura de Agner Fog lo explica bastante bien:

6.8 Puestos de registro parcial (PPro / PII / PIII y Pentium-M temprano)

El bloqueo parcial del registro es un problema que ocurre cuando escribimos en una parte de un registro de 32 bits y luego leemos en todo el registro o en una parte más grande.
Ejemplo:

; Example 6.10a. Partial register stall mov al, byte ptr [mem8] mov ebx, eax ; Partial register stall

Esto da un retraso de 5 a 6 relojes . La razón es que se ha asignado un registro temporal a AL para que sea independiente de AH . La unidad de ejecución tiene que esperar hasta que la escritura en AL haya retirado antes de que sea posible combinar el valor de AL con el valor del resto de EAX .

Comportamiento en diferentes CPU :

Sin cambio de nombre de registro parcial, la dependencia de entrada para la escritura es una dependencia falsa si nunca lee el registro completo. Esto limita el paralelismo a nivel de instrucción porque la reutilización de un registro de 8 o 16 bits para otra cosa no es realmente independiente del punto de vista de la CPU (el código de 16 bits puede acceder a los registros de 32 bits, por lo que debe mantener los valores correctos en la parte superior mitades). Y también, hace que AL y AH no sean independientes. Cuando Intel diseñó la familia P6 (PPro lanzado en 1993), el código de 16 bits todavía era común, por lo que el cambio de nombre del registro parcial era una característica importante para hacer que el código de máquina existente se ejecutara más rápido. (En la práctica, muchos binarios no se vuelven a compilar para las nuevas CPU).

Es por eso que los compiladores en su mayoría evitan escribir registros parciales. Utilizan movzx / movsx siempre que sea posible para ampliar o reducir los valores estrechos a un registro completo para evitar dependencias falsas de registro parcial (AMD) o paradas (familia Intel P6). Por lo tanto, el código de máquina más moderno no se beneficia mucho del cambio de nombre de registro parcial, razón por la cual las CPU Intel recientes están simplificando su lógica de cambio de nombre de registro parcial.

Como señala la respuesta de @ BeeOnRope , los compiladores aún leen registros parciales, porque eso no es un problema. (Leer AH / BH / CH / DH puede agregar un ciclo adicional de latencia en Haswell / Skylake, sin embargo, vea el enlace anterior sobre registros parciales en miembros recientes de la familia Sandybridge).

También tenga en cuenta que la write toma argumentos que, para un GCC x86-64 configurado típicamente, necesitan registros completos de 32 bits y 64 bits, por lo que no se puede ensamblar simplemente en mov dl, 3 . El tamaño está determinado por el tipo de datos, no por el valor de los datos.

Finalmente, en ciertos contextos, C tiene promociones de argumentos por defecto a tener en cuenta, aunque este no es el caso .
En realidad, como señaló RossRidge , la llamada probablemente se hizo sin un prototipo visible.

Su desmontaje es engañoso, como señaló @Jester.
Por ejemplo, mov rdx, 3 es en realidad mov edx, 3 , aunque ambos tienen el mismo efecto, es decir, poner 3 en todo el rdx .
Esto es cierto porque un valor inmediato de 3 no requiere extensión de signo y un MOV r32, imm32 borra implícitamente los 32 bits superiores del registro.