Maschinencode i386 (x86-32), 8 Byte (9 Byte für vorzeichenlose Zeichen)
+ 1B, wenn wir mit b = 0Eingaben umgehen müssen .
Computercode amd64 (x86-64), 9 Byte (10B für vorzeichenlose oder 14B 13B für vorzeichenlose 64B-Ganzzahlen)
10 9B für unsigned auf amd64, das mit beiden Eingängen = 0 abbricht
Die Eingänge sind 32 - Bit - Nicht-Null unterzeichnete ganze Zahlen in eaxund ecx. Ausgabe 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
Diese Schleifenstruktur besteht den Testfall wo nicht ecx = 0. ( divVerursacht eine #DEHardwareausführung beim Teilen durch Null. (Unter Linux liefert der Kernel eine SIGFPE(Gleitkomma-Ausnahme).) Wenn der Loop-Einstiegspunkt direkt vor dem liegt inc, vermeiden wir das Problem. Die x86-64-Version kann damit umgehen kostenlos, siehe unten.
Die Antwort von Mike Shlanta war der Ausgangspunkt dafür . Meine Schleife macht dasselbe wie seine, aber für vorzeichenbehaftete ganze Zahlen, weil cdqein Byte kürzer ist als xor edx,edx. Und ja, es funktioniert korrekt mit einem oder beiden negativen Eingängen. Mikes Version wird schneller laufen und weniger Platz im UOP-Cache beanspruchen ( xchg3 UOPs auf Intel-CPUs und loopauf den meisten CPUs sehr langsam ), aber diese Version gewinnt bei der Größe des Maschinencodes.
Ich habe zunächst nicht bemerkt, dass die Frage 32bit ohne Vorzeichen erfordert . Ein Zurück zu xor edx,edxanstatt cdqwürde ein Byte kosten. divhat die gleiche Größe wie idivund alles andere kann gleich bleiben ( xchgfür die Datenübertragung und inc/loopfunktioniert immer noch).
Interessanterweise haben für 64-Bit-Operanden ( raxund rcx) vorzeichenbehaftete und vorzeichenlose Versionen dieselbe Größe. Die signierte Version benötigt ein REX-Präfix für cqo(2B), die nicht signierte Version kann jedoch weiterhin 2B verwenden xor edx,edx.
Im 64-Bit-Code inc ecxist 2B: Die Einzelbyte- inc r32und dec r32Opcodes wurden als REX-Präfixe neu verwendet. inc/loopspeichert keine Codegröße im 64-Bit-Modus, also könnten Sie es auch test/jnz. Bei 64-Bit-Ganzzahlen wird ein weiteres Byte pro Befehl in REX-Präfixen hinzugefügt, mit Ausnahme von loopoder jnz. Es ist möglich, dass der Rest alle Nullen in den niedrigen 32b hat (zB gcd((2^32), (2^32 + 1))), also müssen wir den gesamten RCX testen und können kein Byte mit speichern test ecx,ecx. Das langsamere jrcxzINSN ist jedoch nur 2B, und wir können es oben in die Schleifeecx=0 einfügen, um es bei der Eingabe zu behandeln :
## 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
Vollständig lauffähiges Testprogramm, einschließlich eines Programms main, mit dem printf("...", gcd(atoi(argv[1]), atoi(argv[2])) ); Quell- und Asm-Ausgabe im Godbolt Compiler Explorer für die Versionen 32 und 64b ausgeführt werden. Getestet und lauffähig für 32bit ( -m32), 64bit ( -m64) und x32 ABI ( -mx32) .
Ebenfalls enthalten: eine Version, bei der nur die wiederholte Subtraktion verwendet wird. Dies ist 9B für den Modus ohne Vorzeichen, auch für den x86-64-Modus, und kann einen seiner Eingänge in einem beliebigen Register annehmen. Es kann jedoch keine der Eingaben verarbeiten, die bei der Eingabe 0 sind (es erkennt, wenn subeine Null erzeugt wird, was x - 0 niemals tut).
GNU C Inline Asm Source für die 32bit Version (kompilieren mit 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;
}
Normalerweise würde ich eine ganze Funktion in asm schreiben, aber GNU C inline asm scheint der beste Weg zu sein, um ein Snippet einzubeziehen, das Ein- / Ausgänge in den von uns gewählten Regs haben kann. Wie Sie sehen, macht die GNU C-Inline-asm-Syntax asm hässlich und laut. Es ist auch ein sehr schwieriger Weg , asm zu lernen .
Es würde tatsächlich kompilieren und im .att_syntax noprefixModus arbeiten, da alle verwendeten Insns entweder Single / No Operand oder sind xchg. Keine wirklich nützliche Beobachtung.