codice macchina i386 (x86-32), 8 byte (9B per non firmato)
+ 1B se dobbiamo gestire b = 0
l'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 eax
e 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
. ( div
provoca #DE
un'esecuzione hardware sulla divisione per zero. (Su Linux, il kernel fornisce SIGFPE
un'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,edx
invece di cdq
costerebbe un byte. div
ha le stesse dimensioni di idiv
, e tutto il resto può rimanere lo stesso ( xchg
per lo spostamento dei dati e inc/loop
ancora funzionare).
È interessante notare che per le versioni operando a 64 bit ( rax
e 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 r32
e gli dec r32
opcode sono stati riproposti come prefissi REX. inc/loop
non 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 loop
o 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' jrcxz
insn più lento è solo 2B e possiamo inserirlo nella parte superiore del ciclo per gestirlo ecx=0
all'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 main
che 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 sub
produce 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 noprefix
Compilerebbe e funzionerebbe in modalità, perché tutti gli insn utilizzati sono operandi singoli / no o xchg
. Non è davvero un'osservazione utile.