¿Utiliza LEA en valores que no son direcciones/punteros?
Estaba tratando de entender cómo funciona la instrucción de cálculo de direcciones, especialmente con leaq
el comando. Luego me confundo cuando veo ejemplos que se utilizan leaq
para realizar cálculos aritméticos. Por ejemplo, el siguiente código C,
long m12(long x) {
return x*12;
}
En asamblea,
leaq (%rdi, %rdi, 2), %rax
salq $2, $rax
Si tengo entendido que es correcto, Leaq debería mover cualquier dirección (%rdi, %rdi, 2)
, que debería ser 2*%rdi+%rdi
, evaluada a %rax
. Lo que me confunde es que dado que el valor x está almacenado en %rdi
, que es solo la dirección de memoria, ¿por qué multiplicar %rdi por 3 y luego desplazar a la izquierda esta dirección de memoria por 2 es igual a x por 12? ¿No es que cuando multiplicamos %rdi
por 3, saltamos a otra dirección de memoria que no contiene el valor x?
lea
(consulte la entrada del manual del conjunto de instrucciones de Intel) es una instrucción de desplazamiento y suma que utiliza sintaxis de operando de memoria y codificación de máquina. Esto explica el nombre, pero no es lo único para lo que sirve. En realidad, nunca accede a la memoria, por lo que es como usarlo&
en C.
Vea, por ejemplo, ¿Cómo multiplicar un registro por 37 usando solo 2 instrucciones leales consecutivas en x86?
En C, es como uintptr_t foo = (uintptr_t) &arr[idx]
. Tenga en cuenta lo &
que le daremos arr + idx
(escalado para el tamaño del objeto, arr
ya que es C, no ASM). En C, esto sería un abuso de la sintaxis y los tipos del lenguaje, pero en x86 los punteros de ensamblado y los números enteros son lo mismo. Todo son solo bytes y depende del programa poner las instrucciones en el orden correcto para obtener resultados útiles.
Dirección efectiva es un término técnico en x86: significa la parte "compensada" de una dirección lógica seg:off, especialmente cuandobase_reg + index*scale + displacement
se necesita un cálculo. por ejemplo,rax + (rcx<<2)
en%gs:(%rax,%rcx,4)
modo de direccionamiento . (Pero EA todavía se aplica a%rdi
forstosb
, o al desplazamiento absoluto paramovabs
carga/almacenamiento, u otros casos sin un modo addr ModRM). Su uso en este contexto no significa que deba ser una dirección de memoria válida/útil, sino que le indica que el cálculo no involucra la base del segmento , por lo que no está calculando unadirección lineal . (Agregar la base seg la haría inutilizable para matemáticas de direcciones reales en un modelo de memoria no plana).
El diseñador/arquitecto original del conjunto de instrucciones de 8086 ( Stephen Morse ) podría o no haber tenido en mente las matemáticas de punteros como el caso de uso principal, pero los compiladores modernos lo consideran simplemente otra opción para hacer aritmética con punteros/enteros, y así deberían los humanos.
(Tenga en cuenta que los modos de direccionamiento de 16 bits no incluyen cambios, solo [BP|BX] + [SI|DI] + disp8/disp16
, por lo que LEA no era tan útil para matemáticas sin puntero antes de 386. Consulte estas preguntas y respuestas para obtener más información sobre los modos de direccionamiento de 32/64 bits, aunque esa respuesta usa Intel sintaxis como [rax + rdi*4]
en lugar de la sintaxis de AT&T utilizada en esta pregunta. El código de máquina x86 es el mismo independientemente de la sintaxis que utilice para crearlo).
Quizás los arquitectos del 8086 simplemente querían exponer el hardware de cálculo de direcciones para usos arbitrarios porque podían hacerlo sin utilizar muchos transistores adicionales. El decodificador ya debe poder decodificar modos de direccionamiento y otras partes de la CPU deben poder realizar cálculos de direcciones. Poner el resultado en un registro en lugar de usarlo con un valor de registro de segmento para el acceso a la memoria no requiere muchos transistores adicionales. Ross Ridge confirma que LEA en el 8086 original reutiliza el hardware de cálculo y decodificación de direcciones efectivas de la CPU.
Tenga en cuenta que la mayoría de las CPU modernas ejecutan LEA en las mismas ALU que las instrucciones normales de adición y desplazamiento . Tienen AGU (unidades de generación de direcciones) dedicadas, pero solo las usan para operandos de memoria reales. Atom en orden es una excepción; LEA se ejecuta antes que las ALU: las entradas deben estar listas antes, pero las salidas también lo están antes. Las CPU de ejecución fuera de orden (todas las x86 modernas) no quieren que LEA interfiera con las cargas/almacenamiento reales, por lo que las ejecutan en una ALU.
lea
tiene buena latencia y rendimiento, pero no tan bueno como add
el mov r32, imm32
de la mayoría de las CPU, por lo que solo utilícelo lea
cuando pueda guardar instrucciones con él en lugar de add
. (Consulte la guía de microarcos x86 de Agner Fog y el manual de optimización de asm y https://uops.info/ ).
Ice Lake mejoró eso para Intel y ahora puede ejecutar LEA en los cuatro puertos ALU.
Las reglas sobre qué tipos de LEA son "complejos", que se ejecutan en menos puertos que pueden manejarlos, varían según la microarquitectura. por ejemplo, 3 componentes (dos + operaciones) es el caso más lento en la familia SnB, tener un índice escalado es el caso de menor rendimiento en Ice Lake. Los núcleos E de Alder Lake (Gracemont) son 4/reloj, pero 1/reloj cuando hay un índice, y latencia de 2 ciclos cuando hay un índice y desplazamiento (ya sea que haya un registro base o no). Zen es más lento cuando hay un índice escalado o 3 componentes. (latencia de 2c y 2/reloj en comparación con 1c y 4/reloj).
La implementación interna es irrelevante, pero es una apuesta segura que la decodificación de los operandos para LEA comparte transistores con los modos de direccionamiento de decodificación para cualquier otra instrucción . (Por lo tanto, se puede reutilizar/compartir hardware incluso en CPU modernas que no se ejecutan lea
en una AGU). Cualquier otra forma de exponer una instrucción de desplazamiento y suma de entradas múltiples habría requerido una codificación especial para los operandos.
Entonces, 386 obtuvo una instrucción ALU de desplazamiento y adición de forma "gratuita" cuando extendió los modos de direccionamiento para incluir el índice escalado, y poder usar cualquier registro en un modo de direccionamiento hizo que LEA también fuera mucho más fácil de usar para los que no son punteros. .
x86-64 obtuvo acceso económico al contador del programa ( en lugar de tener que leer lo que call
se envió ) "gratis" a través de LEA porque agregó el modo de direccionamiento relativo a RIP, lo que hace que el acceso a datos estáticos sea significativamente más barato en código x86-64 independiente de la posición que en PIC de 32 bits. (El relativo a RIP necesita soporte especial en las ALU que manejan LEA, así como en las AGU separadas que manejan direcciones de carga/almacenamiento reales. Pero no se necesitaron nuevas instrucciones).
Es tan bueno para aritmética arbitraria como para punteros, por lo que es un error pensar que está destinado a punteros en estos días . No es un "abuso" o "truco" usarlo para no punteros, porque todo es un número entero en lenguaje ensamblador. Tiene un rendimiento menor que add
, pero es lo suficientemente económico como para usarlo casi todo el tiempo cuando guarda incluso una instrucción. Pero puede guardar hasta tres instrucciones:
;; Intel syntax.
lea eax, [rdi + rsi*4 - 8] ; 3 cycle latency on Intel SnB-family
; 2-component LEA is only 1c latency
;;; without LEA:
mov eax, esi ; maybe 0 cycle latency, otherwise 1
shl eax, 2 ; 1 cycle latency
add eax, edi ; 1 cycle latency
sub eax, 8 ; 1 cycle latency
En algunas CPU AMD, incluso un LEA complejo tiene una latencia de solo 2 ciclos, pero la secuencia de 4 instrucciones tendría una latencia de 4 ciclos desde que esi
está listo hasta que eax
está listo final. De cualquier manera, esto ahorra 3 uops para que el front-end los decodifique y emita, y eso ocupa espacio en el búfer de reorden hasta el retiro.
lea
tiene varios beneficios importantes , especialmente en código de 32/64 bits donde los modos de direccionamiento pueden usar cualquier registro y pueden cambiar:
- no destructivo: salida en un registro que no es una de las entradas . A veces es útil simplemente como copiar y agregar como
lea 1(%rdi), %eax
olea (%rdx, %rbp), %ecx
. - Puede realizar 3 o 4 operaciones en una instrucción (ver arriba).
- Las matemáticas sin modificar EFLAGS pueden ser útiles después de un examen antes de un
cmovcc
. O tal vez en un bucle de agregar con acarreo en CPU con bloqueos de bandera parcial. - x86-64: el código independiente de la posición puede utilizar una LEA relativa a RIP para obtener un puntero a datos estáticos.
7 bytes lea foo(%rip), %rdi
es un poco más grande y más lento que mov $foo, %edi
(5 bytes), por lo que se prefiere mov r32, imm32
en código dependiente de la posición en sistemas operativos donde los símbolos se encuentran en los 32 bits bajos del espacio de direcciones virtuales, como Linux. Es posible que necesites deshabilitar la configuración PIE predeterminada en gcc para usar esto.
En código de 32 bits, mov edi, OFFSET symbol
es igualmente más corto y rápido que lea edi, [symbol]
. (Omita la OFFSET
sintaxis en NASM). RIP-relative no está disponible y las direcciones encajan en un inmediato de 32 bits, por lo que no hay razón para considerar lea
si mov r32, imm32
necesita obtener direcciones de símbolos estáticos en los registros.
Aparte de LEA relativa a RIP en modo x86-64, todos estos se aplican por igual al cálculo de punteros que al cálculo de sumas/desplazamientos de enteros sin puntero.
Ver también elx86<!--> etiqueta wiki para guías/manuales de ensamblaje e información de rendimiento.
Tamaño de operando versus tamaño de dirección para x86-64lea
Consulte también ¿Qué operaciones de enteros en complemento a 2 se pueden utilizar sin poner a cero los bits altos en las entradas, si solo se desea la parte baja del resultado? . El tamaño de dirección de 64 bits y el tamaño de operando de 32 bits es la codificación más compacta (sin prefijos adicionales), por lo que, lea (%rdx, %rbp), %ecx
cuando sea posible, prefiera en lugar de 64 lea (%rdx, %rbp), %rcx
o 32 bits lea (%edx, %ebp), %ecx
.
x86-64 lea (%edx, %ebp), %ecx
siempre es un desperdicio de un prefijo de tamaño de dirección frente a lea (%rdx, %rbp), %ecx
, pero obviamente se requiere un tamaño de dirección/operando de 64 bits para realizar operaciones matemáticas de 64 bits. (El desensamblador objconv de Agner Fog incluso advierte sobre prefijos de tamaño de dirección inútiles en LEA con un tamaño de operando de 32 bits).
Excepto quizás en Ryzen, donde Agner Fog informa que el tamaño de operando de 32 bits lea
en modo de 64 bits tiene un ciclo adicional de latencia. No sé si anular el tamaño de la dirección a 32 bits puede acelerar LEA en modo de 64 bits si necesita truncarlo a 32 bits.
Esta pregunta es casi un duplicado de la muy votada ¿ Cuál es el propósito de la instrucción LEA? , pero la mayoría de las respuestas lo explican en términos de cálculo de direcciones en datos de puntero reales. Ese es sólo un uso.
leaq
no tiene que operar en direcciones de memoria, y calcula una dirección, en realidad no lee el resultado, por lo que hasta que alguien mov
o similar intente usarlo, es solo una forma esotérica de sumar un número más 1, 2, 4 u 8 veces otro número (o el mismo número en este caso). Como puede ver , con frecuencia se "abusa" † con fines matemáticos. 2*%rdi+%rdi
es solo 3 * %rdi
, por lo que se calcula x * 3
sin involucrar la unidad multiplicadora en la CPU.
De manera similar, el desplazamiento a la izquierda, para números enteros, duplica el valor por cada bit desplazado (cada cero agregado a la derecha), gracias a la forma en que funcionan los números binarios (de la misma manera en los números decimales, sumando ceros a la derecha se multiplica por 10).
Entonces, esto es abusar de la leaq
instrucción para realizar la multiplicación por 3, luego cambiar el resultado para lograr una multiplicación adicional por 4, para obtener un resultado final de multiplicar por 12 sin siquiera usar una instrucción de multiplicar (que presumiblemente cree que se ejecutaría más lentamente, y por lo que sé, podría ser correcto; adivinar el compilador suele ser un juego perdido).
† : Para ser claros, no es abuso en el sentido de mal uso , simplemente usarlo de una manera que no se alinea claramente con el propósito implícito que esperarías de su nombre. Está 100% bien usarlo de esta manera.
LEA es para calcular la dirección . No desreferencia la dirección de memoria.
Debería ser mucho más legible en la sintaxis Intel.
m12(long):
lea rax, [rdi+rdi*2]
sal rax, 2
ret
Entonces la primera línea es equivalente a rax = rdi*3
Luego, el desplazamiento hacia la izquierda es multiplicar rax por 4, lo que resulta enrdi*3*4 = rdi*12