codice macchina i386 (x86-32), 8 byte (9B per non firmato)
+ 1B se dobbiamo gestire b = 0l'input.
codice macchina amd64 (x86-64), 9 byte (10B per unsigned o 14B 13B per interi 64b con segno o senza segno)
10 9B per unsigned su amd64 che si interrompe con uno dei due input = 0
Gli ingressi sono a 32 bit non-zero firmato interi eaxe ecx. Uscita in 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
Questa struttura ad anello non ha esito positivo nel caso di test ecx = 0. ( divprovoca #DEun'esecuzione hardware sulla divisione per zero. (Su Linux, il kernel fornisce SIGFPEun'eccezione in virgola mobile). Se il punto di ingresso del ciclo fosse proprio prima del inc, eviteremmo il problema. La versione x86-64 può gestirlo gratuitamente, vedi sotto.
La risposta di Mike Shlanta è stata il punto di partenza per questo . Il mio ciclo fa la stessa cosa della sua, ma per numeri interi con segno perché cdqè un byte più breve di xor edx,edx. E sì, funziona correttamente con uno o entrambi gli ingressi negativi. La versione di Mike funzionerà più velocemente e occuperà meno spazio nella cache uop ( xchgè 3 uops su CPU Intel ed loopè molto lenta sulla maggior parte delle CPU ), ma questa versione vince con le dimensioni del codice macchina.
All'inizio non ho notato che la domanda richiedeva 32 bit senza segno . Tornare indietro xor edx,edxinvece di cdqcosterebbe un byte. divha le stesse dimensioni di idiv, e tutto il resto può rimanere lo stesso ( xchgper lo spostamento dei dati e inc/loopancora funzionare).
È interessante notare che per le versioni operando a 64 bit ( raxe rcx), le versioni firmate e non firmate hanno le stesse dimensioni. La versione firmata necessita di un prefisso REX per cqo(2B), ma la versione non firmata può ancora utilizzare 2B xor edx,edx.
Nel codice a 64 bit, inc ecxè 2B: il singolo byte inc r32e gli dec r32opcode sono stati riproposti come prefissi REX. inc/loopnon salva alcuna dimensione del codice in modalità 64 bit, quindi potresti anche test/jnz. Operando su numeri interi a 64 bit aggiunge un altro byte per istruzione nei prefissi REX, ad eccezione di loopo jnz. È possibile che il resto abbia tutti gli zeri nei 32b bassi (ad es. gcd((2^32), (2^32 + 1))), Quindi abbiamo bisogno di testare l'intero rcx e non possiamo salvare un byte con test ecx,ecx. Tuttavia, l' jrcxzinsn più lento è solo 2B e possiamo inserirlo nella parte superiore del ciclo per gestirlo ecx=0all'entrata :
## 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
Programma di test eseguibile completa con una mainche corre printf("...", gcd(atoi(argv[1]), atoi(argv[2])) ); fonte e l'uscita asm sul Godbolt compilatore Explorer , per i 32 e 64b versioni. Testato e funzionante per 32 bit ( -m32), 64 bit ( -m64) e x32 ABI ( -mx32) .
Incluso anche: una versione che utilizza solo sottrazioni ripetute , che è 9B per unsigned, anche per la modalità x86-64, e può prendere uno dei suoi input in un registro arbitrario. Tuttavia, non è in grado di gestire entrambi gli input che sono 0 alla voce (rileva quando subproduce uno zero, che x - 0 non lo fa mai).
GNU C inline asm source per la versione a 32 bit (compilare 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 scriverei un'intera funzione in asm, ma GNU C inline asm sembra essere il modo migliore per includere uno snippet che può avere in / output in qualsiasi reg scegliamo. Come puoi vedere, la sintassi di GNU C inline asm rende brutta e rumorosa. È anche un modo davvero difficile per imparare l' asm .
.att_syntax noprefixCompilerebbe e funzionerebbe in modalità, perché tutti gli insn utilizzati sono operandi singoli / no o xchg. Non è davvero un'osservazione utile.