Respuestas:
mov
-inmediato es caro para las constantesEsto puede ser obvio, pero aún lo pondré aquí. En general, vale la pena pensar en la representación a nivel de bit de un número cuando necesita inicializar un valor.
eax
con 0
:b8 00 00 00 00 mov $0x0,%eax
debe acortarse ( para el rendimiento y el tamaño del código ) a
31 c0 xor %eax,%eax
eax
con -1
:b8 ff ff ff ff mov $-1,%eax
se puede acortar a
31 c0 xor %eax,%eax
48 dec %eax
o
83 c8 ff or $-1,%eax
O, más generalmente, cualquier valor de signo extendido de 8 bits se puede crear en 3 bytes con push -12
(2 bytes) / pop %eax
(1 byte). Esto incluso funciona para registros de 64 bits sin prefijo REX adicional; push
/ pop
default operand-size = 64.
6a f3 pushq $0xfffffffffffffff3
5d pop %rbp
O dada una constante conocida en un registro, puede crear otra constante cercana usando lea 123(%eax), %ecx
(3 bytes). Esto es útil si necesita un registro a cero y una constante; xor-zero (2 bytes) + lea-disp8
(3 bytes).
31 c0 xor %eax,%eax
8d 48 0c lea 0xc(%eax),%ecx
Consulte también Establecer todos los bits en el registro de CPU en 1 de manera eficiente
dec
, por ejemploxor eax, eax; dec eax
push imm8
/ pop reg
es de 3 bytes, y es fantástico para constantes de 64 bits en x86-64, donde dec
/ inc
es de 2 bytes. Y push r64
/ pop 64
(2 bytes) puede incluso reemplazar un byte de mov r64, r64
3 (3 bytes con REX). Consulte también Establecer todos los bits en el registro de la CPU en 1 de manera eficiente para cosas como lea eax, [rcx-1]
un valor conocido dado eax
(por ejemplo, si necesita un registro a cero y otra constante, solo use LEA en lugar de push / pop
En muchos casos, las instrucciones basadas en acumuladores (es decir, las que toman (R|E)AX
como operando de destino) son 1 byte más cortas que las instrucciones de caso general; vea esta pregunta en StackOverflow.
al, imm8
casos especiales, como or al, 0x20
/ sub al, 'a'
/ cmp al, 'z'-'a'
/ que ja .non_alphabetic
son 2 bytes cada uno, en lugar de 3. El uso al
de datos de caracteres también permite lodsb
y / o stosb
. O use al
para probar algo sobre el byte bajo de EAX, como lodsd
/ test al, 1
/ setnz cl
hace que cl = 1 o 0 para impar / par. Pero en el raro caso de que necesite una respuesta inmediata de 32 bits, entonces seguro op eax, imm32
, como en mi respuesta de clave de croma
El lenguaje de su respuesta es asm (en realidad código de máquina), así que trátelo como parte de un programa escrito en asm, no C-compilado-para-x86. Su función no tiene que ser fácilmente invocable desde C con ninguna convención de llamada estándar. Sin embargo, es una buena ventaja si no le cuesta bytes adicionales.
En un programa asm puro, es normal que algunas funciones auxiliares utilicen una convención de llamadas que sea conveniente para ellos y para su interlocutor. Dichas funciones documentan su convención de llamada (entradas / salidas / clobbers) con comentarios.
En la vida real, incluso los programas asm (creo) tienden a usar convenciones de llamadas consistentes para la mayoría de las funciones (especialmente en diferentes archivos fuente), pero cualquier función importante podría hacer algo especial. En code-golf, está optimizando la basura de una sola función, por lo que obviamente es importante / especial.
Para probar su función desde un programa en C, puede escribir una envoltura que coloque los argumentos en los lugares correctos, guarde / restaure cualquier registro adicional que haya marcado y coloque el valor de retorno e/rax
si aún no estaba allí.
Requerir que DF (indicador de dirección de cadena para lods
/ stos
/ etc.) esté despejado (hacia arriba) en la llamada / ret es normal. Dejarlo sin definir en call / ret estaría bien. Requerir que se borre o establecer en la entrada, pero luego dejarlo modificado cuando regrese sería extraño.
Devolver los valores de FP en x87 st0
es razonable, pero regresar st3
con basura en otro registro x87 no lo es. La persona que llama tendría que limpiar la pila x87. Incluso regresar st0
con registros de pila más altos no vacíos también sería cuestionable (a menos que esté devolviendo valores múltiples).
call
, al igual [rsp]
que su dirección de devolución. Usted puede evitar call
/ ret
x86 usando registro de enlace como lea rbx, [ret_addr]
/ jmp function
y de retorno con jmp rbx
, pero eso no es "razonable". Eso no es tan eficiente como call / ret, por lo que no es algo que posiblemente encuentre en el código real.Casos límite: escriba una función que produzca una secuencia en una matriz, dados los primeros 2 elementos como argumentos de función . Elegí que la persona que llama almacenara el inicio de la secuencia en la matriz y simplemente pasara un puntero a la matriz. Esto definitivamente está doblando los requisitos de la pregunta. Consideré tomar los argumentos empaquetados en xmm0
para movlps [rdi], xmm0
, que también sería una convención de llamada extraña.
Las llamadas al sistema OS X hacen esto ( CF=0
significa que no hay error): ¿se considera una mala práctica usar el registro de banderas como un valor de retorno booleano? .
Cualquier condición que pueda verificarse con un JCC es perfectamente razonable, especialmente si puede elegir una que tenga alguna relevancia semántica para el problema. (por ejemplo, una función de comparación podría establecer marcas, por jne
lo que se tomarán si no fueran iguales).
char
) para ser signo o cero extendido a 32 o 64 bits.Esto no es irrazonable; usar movzx
o movsx
para evitar ralentizaciones de registro parcial es normal en el x86 asm moderno. De hecho, clang / LLVM ya crea un código que depende de una extensión no documentada de la convención de llamadas del Sistema V x86-64: los argumentos más estrechos que 32 bits son signos o cero extendido a 32 bits por el llamante .
Puede documentar / describir la extensión a 64 bits escribiendo uint64_t
o int64_t
en su prototipo si lo desea. por ejemplo, puede usar una loop
instrucción, que usa los 64 bits completos de RCX a menos que use un prefijo de tamaño de dirección para anular el tamaño hasta ECX de 32 bits (sí, realmente, el tamaño de la dirección no es el tamaño del operando).
Tenga en cuenta que long
solo es un tipo de 32 bits en Windows ABI de 64 bits y Linux x32 ABI ; uint64_t
es inequívoco y más corto de escribir que unsigned long long
.
Windows de 32 bits __fastcall
, ya sugerido por otra respuesta : arger entero ecx
y edx
.
x86-64 System V : pasa muchos argumentos en los registros y tiene muchos registros de llamadas que puede usar sin prefijos REX. Más importante aún, en realidad se eligió para permitir que los compiladores en línea memcpy
o memset con la misma rep movsb
facilidad: los primeros 6 argumentos enteros / puntero se pasan en RDI, RSI, RDX, RCX, R8, R9.
Si su función usa lodsd
/ stosd
dentro de un ciclo que ejecuta rcx
tiempos (con la loop
instrucción), puede decir "invocable desde C como int foo(int *rdi, const int *rsi, int dummy, uint64_t len)
con la convención de llamadas del sistema V x86-64". ejemplo: chromakey .
GCC de 32 bits regparm
: argumentos enteros en EAX , ECX, EDX, retorno en EAX (o EDX: EAX). Tener el primer argumento en el mismo registro que el valor de retorno permite algunas optimizaciones, como este caso con un llamador de ejemplo y un prototipo con un atributo de función . Y, por supuesto, AL / EAX es especial para algunas instrucciones.
El Linux x32 ABI utiliza punteros de 32 bits en modo largo, por lo que puede guardar un prefijo REX al modificar un puntero (por ejemplo, caso de uso ). Todavía puede usar un tamaño de dirección de 64 bits, a menos que tenga un entero negativo de 32 bits con cero extendido en un registro (por lo que sería un gran valor sin signo si lo hiciera [rdi + rdx]
).
Tenga en cuenta que push rsp
/ pop rax
es de 2 bytes, y equivalente a mov rax,rsp
, por lo que aún puede copiar registros completos de 64 bits en 2 bytes.
ret 16
; no muestran la dirección de retorno, empujan una matriz, luego push rcx
/ ret
. La persona que llama tendría que conocer el tamaño de la matriz o haber guardado RSP en algún lugar fuera de la pila para encontrarse.
Utilice codificaciones de forma corta de casos especiales para AL / AX / EAX y otras formas cortas e instrucciones de un solo byte
Los ejemplos suponen el modo de 32/64 bits, donde el tamaño de operando predeterminado es de 32 bits. Un prefijo de tamaño de operando cambia la instrucción a AX en lugar de EAX (o al revés en modo de 16 bits).
inc/dec
un registro (que no sea de 8 bits): inc eax
/ dec ebp
. (No x86-64: los 0x4x
bytes del código de operación se reutilizaron como prefijos REX, por lo que inc r/m32
es la única codificación).
8 bits inc bl
es de 2 bytes, utilizando el inc r/m8
código de operación + Modr / M operando codifica . Así que usa inc ebx
para incrementar bl
, si es seguro. (por ejemplo, si no necesita el resultado ZF en los casos en que los bytes superiores pueden ser distintos de cero).
scasd
: e/rdi+=4
, requiere que el registro apunte a memoria legible. A veces es útil incluso si no te importa el resultado de FLAGS (como cmp eax,[rdi]
/ rdi+=4
). Y en el modo de 64 bits, scasb
puede funcionar como un byteinc rdi
, si lodsb o stosb no son útiles.
xchg eax, r32
: Aquí es donde 0x90 NOP vino de: xchg eax,eax
. Ejemplo: reorganice 3 registros con dos xchg
instrucciones en un bucle cdq
/ para GCD en 8 bytes, donde la mayoría de las instrucciones son de un solo byte, incluido un abuso de / en lugar de /idiv
inc ecx
loop
test ecx,ecx
jnz
cdq
: firma-extiende EAX en EDX: EAX, es decir, copia el bit alto de EAX a todos los bits de EDX. Para crear un cero con no negativo conocido, o para obtener un 0 / -1 para agregar / sub o enmascarar. Lección de historia x86: cltq
vs.movslq
, y también AT&T vs. Intel mnemonics para esto y lo relacionado cdqe
.
lodsb / d : como mov eax, [rsi]
/ rsi += 4
sin banderas de golpeteo. (Suponiendo que DF es claro, qué convenciones de llamada estándar requieren en la entrada de funciones). También stosb / d, a veces scas, y más raramente movs / cmps.
push
/ pop reg
. por ejemplo, en modo de 64 bits, push rsp
/ pop rdi
es de 2 bytes, pero mov rdi, rsp
necesita un prefijo REX y es de 3 bytes.
xlatb
existe, pero rara vez es útil. Una tabla de búsqueda grande es algo que debe evitarse. Tampoco he encontrado un uso para AAA / DAA u otras instrucciones BCD empaquetadas o de 2 dígitos ASCII.
1 byte lahf
/ sahf
rara vez son útiles. Usted pude lahf
/ and ah, 1
como una alternativa a setc ah
, pero no es generalmente útil.
Y para CF específicamente, hay sbb eax,eax
que obtener un 0 / -1, o incluso 1-byte no documentado pero universalmente compatible salc
(establecer AL desde Carry) que efectivamente lo hace sbb al,al
sin afectar a las banderas. (Eliminado en x86-64). Usé SALC en el Desafío de apreciación del usuario # 1: Dennis ♦ .
1 byte cmc
/ clc
/ stc
(flip ("complemento"), clear o set CF) rara vez son útiles, aunque encontré un uso para lacmc
adición de precisión extendida con trozos de base 10 ^ 9. Para configurar / borrar incondicionalmente la CF, generalmente haga los arreglos para que eso suceda como parte de otra instrucción, por ejemplo, xor eax,eax
borra CF y EAX. No hay instrucciones equivalentes para otros indicadores de condición, solo DF (dirección de la cadena) e IF (interrupciones). La bandera de transporte es especial para muchas instrucciones; los cambios lo establecen, adc al, 0
pueden agregarlo a AL en 2 bytes, y mencioné anteriormente el SALC indocumentado.
std
/ cld
Parecer rara vez vale la pena . Especialmente en el código de 32 bits, es mejor usarlo dec
en un puntero y un mov
operando fuente de memoria para una instrucción ALU en lugar de configurar DF así lodsb
/ stosb
ir hacia abajo en lugar de hacia arriba. Por lo general, si necesita algo hacia abajo, todavía tiene otro puntero hacia arriba, por lo que necesitaría más de uno std
y cld
en toda la función para usar lods
/ stos
para ambos. En cambio, solo use las instrucciones de la cuerda para la dirección hacia arriba. (Las convenciones de llamada estándar garantizan DF = 0 en la entrada de función, por lo que puede suponer que es gratis sin usar cld
).
En el original 8086, AX fue muy especial: instrucciones como lodsb
/ stosb
, cbw
, mul
/ div
y otros lo utilizan de forma implícita. Ese sigue siendo el caso, por supuesto; x86 actual no ha eliminado ninguno de los códigos de operación de 8086 (al menos ninguno de los documentados oficialmente). Pero las CPU posteriores agregaron nuevas instrucciones que dieron formas mejores / más eficientes de hacer las cosas sin copiarlas o cambiarlas primero a AX. (O a EAX en modo de 32 bits).
por ejemplo, 8086 careció de adiciones posteriores como movsx
/ movzx
para cargar o mover + signo-extender, o 2 y 3 operandos imul cx, bx, 1234
que no producen un resultado de mitad alta y no tienen ningún operando implícito.
Además, el principal cuello de botella de 8086 era la búsqueda de instrucciones, por lo que la optimización del tamaño del código era importante para el rendimiento en ese momento . El diseñador ISA de 8086 (Stephen Morse) gastó mucho espacio de codificación de código de operación en casos especiales para AX / AL, incluyendo códigos de operación especiales (E) AX / AL-destino para todas las instrucciones básicas de ALU de src inmediato inmediato , solo código de operación + inmediato sin byte ModR / M. 2 bytes add/sub/and/or/xor/cmp/test/... AL,imm8
o AX,imm16
o (en modo de 32 bits) EAX,imm32
.
Pero no hay un caso especial EAX,imm8
, por lo que la codificación ModR / M normal de add eax,4
es más corta.
La suposición es que si va a trabajar en algunos datos, lo querrá en AX / AL, por lo que intercambiar un registro con AX es algo que quizás desee hacer, tal vez incluso con más frecuencia que copiar un registro en AX con mov
.
Todo lo relacionado con la codificación de instrucciones 8086 admite este paradigma, desde instrucciones como lodsb/w
todas las codificaciones de casos especiales para inmediatos con EAX hasta su uso implícito incluso para multiplicar / dividir.
No te dejes llevar; No es automáticamente una victoria cambiar todo a EAX, especialmente si necesita usar inmediatos con registros de 32 bits en lugar de 8 bits. O si necesita intercalar operaciones en múltiples variables en registros a la vez. O si está utilizando instrucciones con 2 registros, no inmediatamente.
Pero siempre tenga en cuenta: ¿estoy haciendo algo que sería más corto en EAX / AL? ¿Puedo reorganizar para que tenga esto en AL, o estoy aprovechando mejor AL con lo que ya estoy usando?
Mezcle operaciones de 8 bits y 32 bits libremente para aprovechar cada vez que sea seguro hacerlo (no es necesario llevarlo a cabo en el registro completo o lo que sea).
cdq
Es útil para lo div
que necesita cero edx
en muchos casos.
cdq
antes de no firmar div
si sabe que su dividendo está por debajo de 2 ^ 31 (es decir, no negativo cuando se trata como firmado), o si lo usa antes de establecer eax
un valor potencialmente grande. Normalmente (fuera del código de golf) usaría cdq
como configuración para idiv
, y xor edx,edx
antesdiv
fastcall
convencionesla plataforma x86 tiene muchas convenciones de llamadas . Debe usar aquellos que pasan parámetros en registros. En x86_64, los primeros parámetros se pasan de todos modos en los registros, por lo que no hay problema. En las plataformas de 32 bits, la convención de llamada predeterminada ( cdecl
) pasa los parámetros en la pila, lo que no es bueno para el golf: el acceso a los parámetros en la pila requiere instrucciones largas.
Cuando usas fastcall
en plataformas de 32 bits, generalmente se pasan 2 primeros parámetros ecx
y edx
. Si su función tiene 3 parámetros, puede considerar implementarla en una plataforma de 64 bits.
Prototipos de función C para fastcall
convención (tomado de esta respuesta de ejemplo ):
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU
0100 81C38000 ADD BX,0080
0104 83EB80 SUB BX,-80
Del mismo modo, agregue -128 en lugar de restar 128
< 128
en <= 127
reducir la magnitud de un operando inmediato para cmp
, o gcc siempre prefiere la reordenación se compara para reducir la magnitud incluso si no es -129 frente a -128.
mul
(luego inc
/ dec
para obtener +1 / -1 y cero)Puede cero eax y edx multiplicando por cero en un tercer registro.
xor ebx, ebx ; 2B ebx = 0
mul ebx ; 2B eax=edx = 0
inc ebx ; 1B ebx=1
dará como resultado que EAX, EDX y EBX sean cero en solo cuatro bytes. Puede poner a cero EAX y EDX en tres bytes:
xor eax, eax
cdq
Pero desde ese punto de partida no puede obtener un tercer registro a cero en un byte más, o un registro +1 o -1 en otros 2 bytes. En su lugar, use la técnica mul.
Ejemplo de caso de uso: concatenación de los números de Fibonacci en binario .
Tenga en cuenta que después de que LOOP
finalice un bucle, ECX será cero y puede usarse para cero EDX y EAX; no siempre tiene que crear el primer cero con xor
.
Podemos suponer que la CPU está en un estado predeterminado conocido y documentado basado en la plataforma y el sistema operativo.
Por ejemplo:
DOS http://www.fysnet.net/yourhelp.htm
Linux x86 ELF http://asm.sourceforge.net/articles/startup.html
_start
. Entonces sí, es un juego justo aprovechar eso si estás escribiendo un programa en lugar de una función. Lo hice en Extreme Fibonacci . (En un ejecutable enlazado dinámicamente, ld.so carreras antes de saltar a tu _start
, y lo hace de basura licencia en los registros, pero estática es sólo el código.)
Para sumar o restar 1, use un byte inc
o dec
instrucciones que son más pequeñas que las instrucciones de sumar y sub multibyte.
inc/dec r32
con el número de registro codificado en el código de operación. Entonces inc ebx
es 1 byte, pero inc bl
es 2. Todavía más pequeño que add bl, 1
, por supuesto, para registros distintos de al
. También tenga en cuenta que inc
/ dec
deje CF sin modificar, pero actualice las otras banderas.
lea
para las matemáticasEsta es probablemente una de las primeras cosas que uno aprende sobre x86, pero lo dejo aquí como recordatorio. lea
se puede usar para multiplicar por 2, 3, 4, 5, 8 o 9 y agregar un desplazamiento.
Por ejemplo, para calcular ebx = 9*eax + 3
en una instrucción (en modo de 32 bits):
8d 5c c0 03 lea 0x3(%eax,%eax,8),%ebx
Aquí está sin compensación:
8d 1c c0 lea (%eax,%eax,8),%ebx
¡Guauu! Por supuesto, también lea
se puede utilizar para hacer cálculos matemáticos ebx = edx + 8*eax + 3
para calcular la indexación de matrices.
lea eax, [rcx + 13]
es la versión sin prefijos adicionales para el modo de 64 bits. Tamaño de operando de 32 bits (para el resultado) y tamaño de dirección de 64 bits (para las entradas).
Las instrucciones de bucle y cadena son más pequeñas que las secuencias de instrucciones alternativas. Lo más útil es loop <label>
cuál es más pequeño que la secuencia de dos instrucciones dec ECX
y jnz <label>
, y lodsb
es más pequeño que mov al,[esi]
y inc si
.
mov
los pequeños aparecen inmediatamente en los registros inferiores cuando correspondeSi ya sabe que los bits superiores de un registro son 0, puede usar una instrucción más corta para mover un inmediato a los registros inferiores.
b8 0a 00 00 00 mov $0xa,%eax
versus
b0 0a mov $0xa,%al
push
/ pop
para imm8 a cero bits superioresCrédito a Peter Cordes. xor
/ mov
es 4 bytes, pero push
/ pop
es solo 3!
6a 0a push $0xa
58 pop %eax
mov al, 0xa
es bueno si no lo necesita cero extendido al registro completo. Pero si lo hace, xor / mov es 4 bytes vs. 3 para push imm8 / pop o lea
desde otra constante conocida. Esto podría ser útil en combinación con mul
cero 3 registros en 4 bytes , o cdq
, si necesita muchas constantes, sin embargo.
[0x80..0xFF]
, que no son representables como un imm8 con signo extendido. O si ya conoce los bytes superiores, por ejemplo, mov cl, 0x10
después de una loop
instrucción, porque la única forma de loop
no saltar es cuando se hizo rcx=0
. (Supongo que dijiste esto, pero tu ejemplo usa un xor
). Incluso puede usar el byte bajo de un registro para otra cosa, siempre que la otra cosa lo vuelva a poner a cero (o lo que sea) cuando haya terminado. por ejemplo, mi programa Fibonacci se mantiene -1024
en ebx y usa bl.
xchg eax, r32
), por ejemplo, mov bl, 10
/ dec bl
/ jnz
para que su código no se preocupe por los altos bytes de RBX.
Después de muchas instrucciones aritméticas, el indicador de transporte (sin firmar) y el indicador de desbordamiento (firmado) se configuran automáticamente ( más información ). El indicador de signo y el indicador de cero se establecen después de muchas operaciones aritméticas y lógicas. Esto se puede usar para la ramificación condicional.
Ejemplo:
d1 f8 sar %eax
ZF se establece mediante esta instrucción, por lo que podemos usarlo para la ramificación condicional.
test al,1
; generalmente no obtienes eso gratis. (O and al,1
para crear un número entero 0/1 dependiendo de impar / par.)
test
/ cmp
", entonces eso sería bastante básico para principiantes x86, pero aún así merece un voto positivo.
Esto no es específico para x86, pero es una sugerencia de ensamblaje para principiantes ampliamente aplicable. Si sabe que un ciclo while se ejecutará al menos una vez, reescribiendo el ciclo como un ciclo do-while, con la comprobación de la condición del ciclo al final, a menudo guarda una instrucción de salto de 2 bytes. En un caso especial, incluso podría usarlo loop
.
do{}while()
es el idioma natural en bucle en el ensamblaje (especialmente para la eficiencia). Tenga en cuenta también que 2 bytes jecxz
/ jrcxz
antes de un bucle funciona muy bien loop
para manejar las "necesidades de ejecutar cero veces" caso "de manera eficiente" (en las CPU raras donde loop
no es lento). jecxz
también se puede usar dentro del bucle para implementar awhile(ecx){}
, con jmp
en la parte inferior.
Sistema V x 86 utiliza el sistema de pila y V x86-64 usos rdi
, rsi
, rdx
, rcx
, etc., para los parámetros de entrada, y rax
como valor de retorno, pero es perfectamente razonable utilizar su propia convención de llamada. __fastcall usa ecx
y edx
como parámetros de entrada, y otros compiladores / sistemas operativos usan sus propias convenciones . Use la pila y lo que sea que se registre como entrada / salida cuando sea conveniente.
Ejemplo: el contador de bytes repetitivo , utilizando una convención de llamada inteligente para una solución de 1 byte.
Meta: escritura de entrada en registros , escritura de salida en registros
Otros recursos: notas de Agner Fog sobre convenciones de llamadas
int 0x80
que requiere un montón de configuración.
int 0x80
en código de 32 bits, o syscall
en código de 64 bits, invocar sys_write
, es la única buena manera. Es para lo que solía Extreme Fibonacci . En código de 64 bits __NR_write = 1 = STDOUT_FILENO
, para que puedas mov eax, edi
. O si los bytes superiores de EAX son cero, mov al, 4
en código de 32 bits. También podría call printf
o puts
, supongo, y escribir una respuesta "x86 asm for Linux + glibc". Creo que es razonable no contar el espacio de entrada PLT o GOT, o el código de la biblioteca en sí.
char*buf
y produjera la cadena en eso, con formato manual. p. ej. de esta manera (torpemente optimizado para la velocidad) asm FizzBuzz , donde puse los datos de la cadena en el registro y luego los almacené mov
, porque las cadenas eran cortas y de longitud fija.
CMOVcc
y conjuntos condicionalesSETcc
Esto es más un recordatorio para mí, pero existen instrucciones de conjuntos condicionales y existen instrucciones de movimiento condicionales en los procesadores P6 (Pentium Pro) o posteriores. Hay muchas instrucciones que se basan en uno o más de los indicadores establecidos en EFLAGS.
cmov
tiene un código de operación de 2 bytes ( 0F 4x +ModR/M
), por lo que tiene un mínimo de 3 bytes. Pero la fuente es r / m32, por lo que puede cargar condicionalmente en 3 bytes. Aparte de la ramificación, setcc
es útil en más casos que cmovcc
. Aún así, considere todo el conjunto de instrucciones, no solo las instrucciones de referencia 386. (Aunque las instrucciones SSE2 y BMI / BMI2 son tan grandes que rara vez son útiles. rorx eax, ecx, 32
Es de 6 bytes, más largo que mov + ror. Agradable para el rendimiento, no para el golf a menos que POPCNT o PDEP salven muchos isns)
setcc
.
jmp
bytes organizando en if / then en lugar de if / then / elseEsto es ciertamente muy básico, solo pensé en publicar esto como algo en lo que pensar al jugar golf. Como ejemplo, considere el siguiente código directo para decodificar un carácter de dígito hexadecimal:
cmp $'A', %al
jae .Lletter
sub $'0', %al
jmp .Lprocess
.Lletter:
sub $('A'-10), %al
.Lprocess:
movzbl %al, %eax
...
Esto puede acortarse en dos bytes dejando que un caso "entonces" caiga en un caso "else":
cmp $'A', %al
jb .digit
sub $('A'-'0'-10), %eax
.digit:
sub $'0', %eax
movzbl %al, %eax
...
sub
latencia adicional en la ruta crítica para un caso no forma parte de una cadena de dependencia transportada por bucle (como aquí, donde cada dígito de entrada es independiente hasta que se fusionan fragmentos de 4 bits ) Pero supongo que +1 de todos modos. Por cierto, su ejemplo tiene una optimización perdida por separado: si de movzx
todos modos va a necesitar un al final, entonces sub $imm, %al
no use EAX para aprovechar la codificación de 2 bytes sin modrm op $imm, %al
.
cmp
haciendo sub $'A'-10, %al
; jae .was_alpha
; add $('A'-10)-'0'
. (Creo que tengo la lógica correcta). Tenga en cuenta que 'A'-10 > '9'
no hay ambigüedad. Restar la corrección de una letra envolverá un dígito decimal. Así que esto es seguro si asumimos que nuestra entrada es hexadecimal válida, al igual que la suya.
Puede obtener objetos secuenciales de la pila configurando esi en esp, y realizando una secuencia de lodsd / xchg reg, eax.
pop eax
/ pop edx
/ ...? Si necesita dejarlos en la pila, puede push
recuperarlos todos después para restaurar ESP, aún 2 bytes por objeto sin necesidad mov esi,esp
. ¿O quiso decir para objetos de 4 bytes en código de 64 bits donde pop
obtendría 8 bytes? Por cierto, incluso puede usar pop
para recorrer un búfer con un mejor rendimiento que lodsd
, por ejemplo, para la adición de precisión extendida en Extreme Fibonacci
Para copiar un registro de 64 bits, use push rcx
; pop rdx
en lugar de un 3 byte mov
.
El tamaño de operando predeterminado de push / pop es de 64 bits sin necesidad de un prefijo REX.
51 push rcx
5a pop rdx
vs.
48 89 ca mov rdx,rcx
(Un prefijo de tamaño de operando puede anular el tamaño push / pop a 16 bits, pero el tamaño de operando push / pop de 32 bits no se puede codificar en modo de 64 bits, incluso con REX.W = 0).
Si uno o ambos registros son r8
... r15
, úselos mov
porque push y / o pop necesitarán un prefijo REX. En el peor de los casos, esto realmente pierde si ambos necesitan prefijos REX. Obviamente, normalmente debe evitar r8..r15 de todos modos en el código de golf.
Puede mantener su fuente más legible mientras se desarrolla con esto macro NASM . Solo recuerda que pisa los 8 bytes debajo de RSP. (En la zona roja en x86-64 System V). Pero en condiciones normales es un reemplazo directo para 64 bits mov r64,r64
omov r64, -128..127
; mov %1, %2 ; use this macro to copy 64-bit registers in 2 bytes (no REX prefix)
%macro MOVE 2
push %2
pop %1
%endmacro
Ejemplos:
MOVE rax, rsi ; 2 bytes (push + pop)
MOVE rbp, rdx ; 2 bytes (push + pop)
mov ecx, edi ; 2 bytes. 32-bit operand size doesn't need REX prefixes
MOVE r8, r10 ; 4 bytes, don't use
mov r8, r10 ; 3 bytes, REX prefix has W=1 and the bits for reg and r/m being high
xchg eax, edi ; 1 byte (special xchg-with-accumulator opcodes)
xchg rax, rdi ; 2 bytes (REX.W + that)
xchg ecx, edx ; 2 bytes (normal xchg + modrm)
xchg rcx, rdx ; 3 bytes (normal REX + xchg + modrm)
La xchg
parte del ejemplo es porque a veces necesita obtener un valor en EAX o RAX y no le importa preservar la copia anterior. Sin embargo, push / pop no te ayuda a intercambiar.
push 200; pop edx
- 3 bytes para la inicialización.