Maschinencode i386 (x86-32), 8 Byte (9 Byte für vorzeichenlose Zeichen)
+ 1B, wenn wir mit b = 0
Eingaben 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 eax
und 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
. ( div
Verursacht eine #DE
Hardwareausfü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 cdq
ein 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 ( xchg
3 UOPs auf Intel-CPUs und loop
auf 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,edx
anstatt cdq
würde ein Byte kosten. div
hat die gleiche Größe wie idiv
und alles andere kann gleich bleiben ( xchg
für die Datenübertragung und inc/loop
funktioniert immer noch).
Interessanterweise haben für 64-Bit-Operanden ( rax
und 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 ecx
ist 2B: Die Einzelbyte- inc r32
und dec r32
Opcodes wurden als REX-Präfixe neu verwendet. inc/loop
speichert 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 loop
oder 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 jrcxz
INSN 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 sub
eine 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 noprefix
Modus arbeiten, da alle verwendeten Insns entweder Single / No Operand oder sind xchg
. Keine wirklich nützliche Beobachtung.