Código de máquina i386 (x86-32), 8 bytes (9B para sin firmar)
+ 1B si necesitamos manejar la b = 0
entrada.
Código de máquina amd64 (x86-64), 9 bytes (10B para sin signo, o 14B 13B para enteros de 64b con signo o sin signo)
10 9B para unsigned en amd64 que se rompe con input = 0
Las entradas son enteros con signo distinto de cero de 32 bits en eax
y ecx
. La producción en eax
.
## 32bit code, signed integers: eax, ecx
08048420 <gcd0>:
8048420: 99 cdq ; shorter than xor edx,edx
8048421: f7 f9 idiv ecx
8048423: 92 xchg edx,eax ; there's a one-byte encoding for xchg eax,r32. So this is shorter but slower than a mov
8048424: 91 xchg ecx,eax ; eax = divisor(from ecx), ecx = remainder(from edx), edx = quotient(from eax) which we discard
; loop entry point if we need to handle ecx = 0
8048425: 41 inc ecx ; saves 1B vs. test/jnz in 32bit mode
8048426: e2 f8 loop 8048420 <gcd0>
08048428 <gcd0_end>:
; 8B total
; result in eax: gcd(a,0) = a
Esta estructura de bucle falla el caso de prueba donde ecx = 0
. ( div
provoca una #DE
ejecución de hardware en dividir entre cero. (En Linux, el kernel entrega una SIGFPE
(excepción de punto flotante)). Si el punto de entrada del bucle estaba justo antes delinc
, evitaríamos el problema. La versión x86-64 puede manejarlo gratis, ver abajo.
La respuesta de Mike Shlanta fue el punto de partida para esto . Mi bucle hace lo mismo que el suyo, pero para enteros con signo porque cdq
es un byte más corto que xor edx,edx
. Y sí, funciona correctamente con una o ambas entradas negativas. La versión de Mike se ejecutará más rápido y ocupará menos espacio en la caché de UOP ( xchg
es de 3 uops en las CPU de Intel, y loop
es realmente lenta en la mayoría de las CPU ), pero esta versión gana en tamaño de código de máquina.
Al principio no me di cuenta de que la pregunta requería 32 bits sin firmar . Volver a en xor edx,edx
lugar de cdq
costaría un byte. div
es del mismo tamaño que idiv
, y todo lo demás puede permanecer igual ( xchg
para el movimiento de datos yinc/loop
seguir funcionando).
Curiosamente, para el tamaño de operando de 64 bits ( rax
y rcx
), las versiones firmadas y sin firmar son del mismo tamaño. La versión firmada necesita un prefijo REX para cqo
(2B), pero la versión sin firmar aún puede usar 2B xor edx,edx
.
En el código de 64 bits, inc ecx
es 2B: el byte único inc r32
y los códigos de dec r32
operación se reutilizaron como prefijos REX. inc/loop
no guarda ningún tamaño de código en modo de 64 bits, por lo que también podría hacerlo test/jnz
. Operar en enteros de 64 bits agrega otro byte por instrucción en los prefijos REX, excepto para loop
o jnz
. Es posible que el resto tenga todos los ceros en los bajos 32b (por ejemplo gcd((2^32), (2^32 + 1))
), por lo que debemos probar todo el rcx y no podemos guardar un byte con test ecx,ecx
. Sin embargo, el jrcxz
insn más lento es solo 2B, y podemos ponerlo en la parte superior del ciclo para manejarlo ecx=0
en la entrada :
## 64bit code, unsigned 64 integers: rax, rcx
0000000000400630 <gcd_u64>:
400630: e3 0b jrcxz 40063d <gcd_u64_end> ; handles rcx=0 on input, and smaller than test rcx,rcx/jnz
400632: 31 d2 xor edx,edx ; same length as cqo
400634: 48 f7 f1 div rcx ; REX prefixes needed on three insns
400637: 48 92 xchg rdx,rax
400639: 48 91 xchg rcx,rax
40063b: eb f3 jmp 400630 <gcd_u64>
000000000040063d <gcd_u64_end>:
## 0xD = 13 bytes of code
## result in rax: gcd(a,0) = a
Programa de prueba ejecutable completo, incluyendo una main
que corre printf("...", gcd(atoi(argv[1]), atoi(argv[2])) );
la fuente y la salida de ASM en el Godbolt Compilador Explorador , para los 32 y 64b versiones. Probado y funcionando para 32bit ( -m32
), 64bit ( -m64
) y el x32 ABI ( -mx32
) .
También se incluye: una versión que usa solo resta repetida , que es 9B para sin signo, incluso para el modo x86-64, y puede tomar una de sus entradas en un registro arbitrario. Sin embargo, no puede manejar que ninguna entrada sea 0 en la entrada (detecta cuandosub
produce un cero, que x - 0 nunca lo hace).
Fuente asm en línea de GNU C para la versión de 32 bits (compilar con gcc -m32 -masm=intel
)
int gcd(int a, int b) {
asm (// ".intel_syntax noprefix\n"
// "jmp .Lentry%=\n" // Uncomment to handle div-by-zero, by entering the loop in the middle. Better: `jecxz / jmp` loop structure like the 64b version
".p2align 4\n" // align to make size-counting easier
"gcd0: cdq\n\t" // sign extend eax into edx:eax. One byte shorter than xor edx,edx
" idiv ecx\n"
" xchg eax, edx\n" // there's a one-byte encoding for xchg eax,r32. So this is shorter but slower than a mov
" xchg eax, ecx\n" // eax = divisor(ecx), ecx = remainder(edx), edx = garbage that we will clear later
".Lentry%=:\n"
" inc ecx\n" // saves 1B vs. test/jnz in 32bit mode, none in 64b mode
" loop gcd0\n"
"gcd0_end:\n"
: /* outputs */ "+a" (a), "+c"(b)
: /* inputs */ // given as read-write outputs
: /* clobbers */ "edx"
);
return a;
}
Normalmente escribiría una función completa en asm, pero GNU C inline asm parece ser la mejor manera de incluir un fragmento que puede tener entradas / salidas en cualquier reg que elijamos. Como puede ver, la sintaxis asm en línea de GNU C hace que sea feo y ruidoso. También es una forma realmente difícil de aprender asm .
En realidad, se compilaría y funcionaría en .att_syntax noprefix
modo, ya que todas las entradas utilizadas son de un solo operando o no xchg
. No es realmente una observación útil.