¿Se requiere un signo o extensión cero al agregar un desplazamiento de 32 bits a un puntero para el ABI x86-64?

Resumen: estaba buscando código ensamblador para guiar mis optimizaciones y ver muchas extensiones de signo o cero al agregar int32 a un puntero.

void Test(int *out, int offset) { out[offset] = 1; } ------------------------------------- movslq %esi, %rsi movl $1, (%rdi,%rsi,4) ret 

Al principio, pensé que mi comstackdor tenía problemas para agregar enteros de 32 bits a 64 bits, pero he confirmado este comportamiento con Intel ICC 11, ICC 14 y GCC 5.3.

Este hilo confirma mis hallazgos, pero no está claro si es necesario el signo o la extensión cero. Esta extensión de signo / cero solo sería necesaria si los 32 bits superiores aún no están configurados. ¿Pero no sería el x86-64 ABI lo suficientemente inteligente como para exigir eso?

Soy un poco reacio a cambiar todas las compensaciones de mi puntero a ssize_t porque los derrames de registros boostán la huella de caché del código.

Sí, debe suponer que los 32 bits altos de un registro arg o de valor de retorno contienen basura. Sin embargo, puedes dejar basura en los 32 puntos altos cuando llamas o regresas.

Necesita firmar o extender cero a 64 bits para usar el valor en una dirección efectiva de 64 bits. En el ABI x32 , gcc utiliza con frecuencia direcciones efectivas de 32 bits en lugar de utilizar el tamaño de operando de 64 bits para cada instrucción que modifique un entero potencialmente negativo utilizado como índice de matriz.


El estandar:

El x86-64 SysV ABI solo dice algo sobre qué partes de un registro se ponen a cero para _Bool (también conocido como bool ). Página 20:

Cuando se devuelve o pasa un valor de tipo _Bool en un registro o en la stack, el bit 0 contiene el valor de verdad y los bits 1 a 7 deben ser cero (nota 14: otros bits quedan sin especificar, por lo que el lado del consumidor de esos valores puede confíe en que sea 0 o 1 cuando se trunca a 8 bit)

Además, las cosas sobre %al mantener el número de registros de FP args para funciones varargs, no el %rax .

Hay un tema abierto de github sobre esta pregunta exacta en la página de github para los documentos de ABI x32 y x86-64 .

El ABI no impone requisitos o garantías adicionales sobre los contenidos de las partes altas de los registros de números enteros o vectores que contienen args o valores de retorno, por lo que no hay ninguno. Tengo la confirmación de este hecho por correo electrónico de Michael Matz (uno de los mantenedores de ABI): “En general, si el ABI no dice que se especifique algo, no puede confiar en él”.

También confirmó que, por ejemplo, el uso del clang> = 3.6 de un addps que podría ralentizar o generar excepciones de FP adicionales con basura en elementos altos es un error (lo que me recuerda que debo informarlo). Agrega que esto fue un problema una vez con la implementación de AMD de una función matemática glibc. El código C normal puede dejar basura en elementos altos de regs vectores al pasar argumentos double escalares o float .


Comportamiento real que aún no está documentado en el estándar:

Los argumentos de función estrecha, incluso _Bool / bool , se firman o se extienden por cero a 32 bits. clang incluso hace código que depende de este comportamiento (desde 2007, al parecer) . ICC17 no lo hace , por lo que ICC y clang no son compatibles con ABI , incluso para C. No invoque las funciones comstackdas por clang del código comstackdo por ICC para el x86-64 SysV ABI, si alguno de los primeros 6 números enteros args son más estrechos que 32 bits.

Esto no se aplica a los valores de retorno, solo args: gcc y clang ambos suponen que los valores de retorno que reciben solo tienen datos válidos hasta el ancho del tipo. gcc hará que las funciones devuelvan caracteres que dejen basura en los 24 bits de %eax , por ejemplo.

Un hilo reciente en el grupo de discusión de ABI fue una propuesta para aclarar las reglas para extender args de 8 y 16 bits a 32 bits, y tal vez modificar realmente el ABI para requerir esto. Los principales comstackdores (excepto ICC) ya lo hacen, pero sería un cambio en el contrato entre llamantes y callejeros.

Aquí hay un ejemplo (compruébalo con otros comstackdores o modifica el código en Godbolt Compiler Explorer , donde he incluido muchos ejemplos simples que solo demuestran una pieza del rompecabezas, y esto demuestra mucho):

 extern short fshort(short a); extern unsigned fuint(unsigned int a); extern unsigned short array_us[]; unsigned short lookupu(unsigned short a) { unsigned int a_int = a + 1234; a_int += fshort(a); // NOTE: not the same calls as the signed lookup return array_us[a + fuint(a_int)]; } # clang-3.8 -O3 for x86-64. arg in %rdi. (Actually in %di, zero-extended to %edi by our caller) lookupu(unsigned short): pushq %rbx # save a call-preserved reg for out own use. (Also aligns the stack for another call) movl %edi, %ebx # If we didn't assume our arg was already zero-extended, this would be a movzwl (aka movzx) movswl %bx, %edi # sign-extend to call a function that takes signed short instead of unsigned short. callq fshort(short) cwtl # Don't trust the upper bits of the return value. (This is cdqe, Intel syntax. eax = sign_extend(ax)) leal 1234(%rbx,%rax), %edi # this is the point where we'd get a wrong answer if our arg wasn't zero-extended. gcc doesn't assume this, but clang does. callq fuint(unsigned int) addl %ebx, %eax # zero-extends eax to 64bits movzwl array_us(%rax,%rax), %eax # This zero-extension (instead of just writing ax) is *not* for correctness, just for performance: avoid partial-register slowdowns if the caller reads eax popq %rbx retq 

Nota: movzwl array_us(,%rax,2) sería equivalente, pero no más pequeño. Si pudiéramos depender de los bits altos de %rax siendo cero en el valor de retorno de fuint() , el comstackdor podría haber usado array_us(%rbx, %rax, 2) lugar de usar add insn.


Implicaciones de rendimiento

Dejar indefinido el high32 es intencional, y creo que es una buena decisión de diseño.

Ignorar el alto 32 es gratis cuando se realizan operaciones de 32 bits. Una operación de 32 bits de cero extiende su resultado a 64 bits de forma gratuita , por lo que solo necesita un mov edx, edi o algo más si hubiera podido usar el reg directamente en un modo de direccionamiento de 64 bits o en una operación de 64 bits.

Algunas funciones no evitarán que insns tengan sus argumentos ya extendidos a 64 bits, por lo que es un desperdicio potencial para las personas que llaman tener que hacerlo siempre. Algunas funciones usan sus argumentos de una manera que requiere la extensión opuesta de la firma del argumento, por lo que dejarlo al destinatario para decidir qué hacer funciona bien.

Sin embargo, la extensión cero a 64 bits independientemente de la firma podría ser gratuita para la mayoría de las personas que llaman, y podría haber sido una buena opción de diseño de ABI. Como arg regs son destruidos de todos modos, la persona que llama ya necesita hacer algo adicional si quiere mantener un valor de 64 bits completo en una llamada donde solo pasa el bajo 32. Por lo tanto, normalmente solo cuesta extra cuando necesita un 64-bit resultado para algo antes de la llamada, y luego pase una versión truncada a una función. En x86-64 SysV, puede generar su resultado en RDI y usarlo, y luego call foo que solo verá el EDI.

Los tamaños de operandos de 16 bits y 8 bits a menudo dan lugar a dependencias falsas (AMD, P4 o Silvermont, y luego a la familia SnB), o registros parciales (pre SnB) o ralentizaciones menores (Sandybridge), por lo que el comportamiento no documentado requerir que los tipos 8 y 16b se extiendan a 32b para pasar arg tiene sentido. Ver ¿Por qué GCC no usa registros parciales? para más detalles sobre esas microarchitectures.


Esto probablemente no sea un gran problema para el código de tamaño en código real, ya que las funciones pequeñas son / deberían ser static inline , y las inserciones de manejo de arg son una pequeña parte de las funciones más grandes . La optimización entre procedimientos puede eliminar la sobrecarga entre llamadas cuando el comstackdor puede ver ambas definiciones, incluso sin alineación. (IDK qué bien los comstackdores lo hacen en la práctica).

No estoy seguro de si cambiar las firmas de función para usar uintptr_t ayudará o perjudicará el rendimiento general con punteros de 64 bits. No me preocuparía el espacio de stack para escalares. En la mayoría de las funciones, el comstackdor empuja / coloca suficientes registros conservados en la llamada (como %rbx y %rbp ) para mantener sus propias variables en vivo en los registros. Un pequeño espacio extra para derrames de 8B en lugar de 4B es insignificante.

En cuanto al tamaño del código, trabajar con valores de 64 bits requiere un prefijo REX en algunos insns que de otro modo no lo hubieran necesitado. La extensión cero a 64 bits ocurre de forma gratuita si se requieren operaciones en un valor de 32 bits antes de que se use como un índice de matriz. La extensión de la señal siempre requiere una instrucción adicional si es necesaria. Pero los comstackdores pueden extenderse y trabajar con él como un valor firmado de 64 bits desde el comienzo para guardar las instrucciones, a costa de necesitar más prefijos REX. (El desbordamiento firmado es UB, no está definido para envolverse, por lo que los comstackdores a menudo pueden evitar volver a hacer la extensión de signo dentro de un bucle con un int i que usa arr[i] ).

Las CPU modernas generalmente se preocupan más por la cantidad de entradas que por el tamaño, dentro de lo razonable. El código caliente a menudo se ejecutará desde la memoria caché uop en las CPU que los tienen. Aún así, un código más pequeño puede mejorar la densidad en el caché uop. Si puede guardar el tamaño del código sin usar insns más o más lentos, entonces es una ganancia, pero por lo general no vale la pena sacrificar nada más a menos que sea un gran tamaño de código.

Como quizás una instrucción LEA adicional para permitir el direccionamiento [reg + disp8] para una docena de instrucciones posteriores, en lugar de disp32 . O xor eax,eax antes de múltiples mov [rdi+n], 0 instrucciones para reemplazar el imm32 = 0 con una fuente de registro. (Especialmente si eso permite la micro-fusión donde no sería posible con un RIP-relative + immediate, porque lo que realmente importa es el conteo de uop en el frente, no el conteo de instrucciones).

Como el comentario de EOF indica que el comstackdor no puede suponer que los 32 bits superiores de un registro de 64 bits utilizado para pasar un argumento de 32 bits tiene algún valor particular. Eso hace que el signo o la extensión cero sean necesarios.

La única manera de evitar esto sería usar un tipo de 64 bits para el argumento, pero esto mueve el requisito de extender el valor a la persona que llama, que puede no ser una mejora. Sin embargo, no me preocuparía demasiado por el tamaño de los registros, ya que de la forma en que lo está haciendo ahora es más probable que después de la extensión el valor original esté muerto y se derrame el valor extendido de 64 bits. . Incluso si no está muerto, el comstackdor puede seguir prefiriendo dertwigr el valor de 64 bits.

Si realmente está preocupado por la huella de su memoria y no necesita el espacio de direcciones de 64 bits más grande, puede ver el ABI x32 que usa los tipos de ILP32 pero admite todo el conjunto de instrucciones de 64 bits.