Kod maszynowy i386 (x86-32), 8 bajtów (9B dla niepodpisanych)
+ 1B, jeśli musimy obsłużyć b = 0
dane wejściowe.
amd64 (x86-64) kod maszynowy, 9 bajtów (10B dla niepodpisanych lub 14B 13B dla liczb całkowitych 64b podpisanych lub niepodpisanych)
10 9B dla bez znaku na amd64, który psuje się przy obu wejściach = 0
Wejścia są 32bit niezerowe podpisane liczby całkowite eax
i ecx
. Wyjście w 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
Ta struktura pętli zawodzi w przypadku testowym, w którym ecx = 0
. ( div
powoduje wykonanie #DE
sprzętu przy dzieleniu przez zero. (W Linuksie jądro dostarcza SIGFPE
(wyjątek zmiennoprzecinkowy)). Jeśli punkt wejścia do pętli był tuż przed inc
, uniknęlibyśmy problemu. Wersja x86-64 może sobie z tym poradzić za darmo, patrz poniżej.
Odpowiedź Mike Shlanta była punktem wyjścia . Moja pętla robi to samo, co jego, ale dla liczb całkowitych ze cdq
znakiem, ponieważ jest o jeden bajt krótsza niż xor edx,edx
. I tak, działa poprawnie z jednym lub dwoma wejściami ujemnymi. Wersja Mike'a będzie działać szybciej i zajmie mniej miejsca w pamięci podręcznej uop ( xchg
jest 3 uops na procesorach Intela i loop
jest naprawdę wolna na większości procesorów ), ale ta wersja wygrywa przy rozmiarze kodu maszynowego.
Z początku nie zauważyłem, że pytanie wymaga niepodpisanego 32-bitowego. Powrót do xor edx,edx
zamiast cdq
kosztowałby jeden bajt. div
ma taki sam rozmiar jak idiv
i wszystko inne może pozostać niezmienione ( xchg
do przenoszenia danych i inc/loop
nadal działać).
Co ciekawe, dla 64- bitowego rozmiaru operandu ( rax
i rcx
) wersje podpisane i niepodpisane mają ten sam rozmiar. Podpisana wersja wymaga prefiksu REX dla cqo
(2B), ale wersja bez znaku może nadal używać 2B xor edx,edx
.
W kodzie 64-bitowym inc ecx
jest 2B: jednobajtowe inc r32
i dec r32
opcodes zostały zmienione na prefiksy REX. inc/loop
nie zapisuje żadnego rozmiaru kodu w trybie 64-bitowym, więc równie dobrze możesz test/jnz
. Operowanie na 64-bitowych liczbach całkowitych dodaje kolejny jeden bajt na instrukcję w prefiksach REX, z wyjątkiem loop
lub jnz
. Reszta może mieć wszystkie zera na niskim 32b (np. gcd((2^32), (2^32 + 1))
), Więc musimy przetestować cały rcx i nie możemy zapisać bajtu test ecx,ecx
. Jednak wolniejszy jrcxz
insn to tylko 2B i możemy go umieścić na górze pętli, aby obsłużyć ecx=0
przy wejściu :
## 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
W pełni działający program testowy, w tym program main
uruchamiający printf("...", gcd(atoi(argv[1]), atoi(argv[2])) );
źródło i asm w programie Godbolt Compiler Explorer , dla wersji 32 i 64b. Testowane i działające dla 32-bitowych ( -m32
), 64-bitowych ( -m64
) i x32 ABI ( -mx32
) .
Zawiera także: wersję wykorzystującą tylko powtarzane odejmowanie , która wynosi 9B dla niepodpisanego, nawet dla trybu x86-64, i może przyjmować jedno z danych wejściowych w dowolnym rejestrze. Jednak nie może obsłużyć żadnego z wejść przy wejściu 0 (wykrywa, kiedy sub
tworzy zero, czego x-0 nigdy nie robi).
GNU C wbudowane źródło asm dla wersji 32-bitowej (kompilacja z 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;
}
Normalnie napisałbym całą funkcję w asm, ale wbudowany asm GNU C wydaje się być najlepszym sposobem na włączenie fragmentu, który może mieć wejścia / wyjścia w dowolnych regach, które wybieramy. Jak widać, wbudowana składnia asm GNU C powoduje, że asm jest brzydki i głośny. To także bardzo trudny sposób na naukę asm .
W rzeczywistości skompilowałby się i działałby w .att_syntax noprefix
trybie, ponieważ wszystkie użyte insny to albo pojedynczy / brak operandu, albo xchg
. Niezbyt przydatna obserwacja.