Code machine i386 (x86-32), 8 octets (9 b pour non signé)
+ 1B si nous devons gérer b = 0
les entrées.
amd64 (x86-64) code machine, 9 octets (10B pour non signé, ou 14B 13B pour des entiers non signés 64b signés ou)
10 9B pour unsigned sur amd64 qui rompt avec l'une des entrées = 0
Les entrées sont des entiers signés 32 bits non nuls dans eax
et ecx
. Sortie en 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
Cette structure de boucle échoue dans le cas de test où ecx = 0
. ( div
provoque une #DE
exécution matérielle lors d'une division par zéro. (Sous Linux, le noyau fournit une SIGFPE
(exception de virgule flottante)). Si le point d'entrée de la boucle était juste avant le inc
, nous éviterions le problème. La version x86-64 peut le gérer. gratuitement, voir ci-dessous.
La réponse de Mike Shlanta a été le point de départ pour cela . Ma boucle fait la même chose que la sienne, mais pour les entiers signés car cdq
est un octet plus court que xor edx,edx
. Et oui, cela fonctionne correctement avec une ou les deux entrées négatives. La version de Mike fonctionnera plus rapidement et prendra moins d’espace dans le cache uop ( xchg
3 uops sur les processeurs Intel et loop
très lente sur la plupart des processeurs ), mais cette version gagne à la taille du code machine.
Je n'avais pas remarqué au début que la question demandait 32 bits non signés . Retourner à xor edx,edx
au lieu de cdq
coûterait un octet. div
est de la même taille que idiv
, et tout le reste peut rester identique ( xchg
pour le transfert de données et inc/loop
fonctionne toujours).
Fait intéressant, pour les opérandes de taille 64 bits ( rax
et rcx
), les versions signée et non signée ont la même taille. La version signée nécessite un préfixe REX pour cqo
(2B), mais la version non signée peut toujours utiliser 2B xor edx,edx
.
Dans le code 64 bits, inc ecx
correspond à 2B: le code sur un octet inc r32
et les dec r32
codes opération ont été réaffectés en tant que préfixes REX. inc/loop
n'enregistre aucune taille de code en mode 64 bits, donc vous pourriez aussi bien test/jnz
. Opérer sur des entiers 64 bits ajoute un octet supplémentaire par instruction dans les préfixes REX, à l'exception de loop
ou jnz
. Il est possible que le reste ait tous les zéros dans le bas 32b (par exemple gcd((2^32), (2^32 + 1))
), nous avons donc besoin de tester tout le rcx et nous ne pouvons pas sauvegarder un octet avec test ecx,ecx
. Cependant, l’ jrcxz
insn le plus lent n’est que 2B, et nous pouvons le placer en haut de la boucle pour gérer ecx=0
en entrée :
## 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
Programme de test complet exécutable, y compris un programme main
qui printf("...", gcd(atoi(argv[1]), atoi(argv[2])) );
utilise les sources et les sorties asm sur Godbolt Compiler Explorer , pour les versions 32 et 64b. Testé et fonctionnant pour 32bit ( -m32
), 64bit ( -m64
) et x32 ABI ( -mx32
) .
Sont également inclus: une version utilisant uniquement la soustraction répétée , soit 9B pour le type non signé, même en mode x86-64, et peut prendre l’une de ses entrées dans un registre arbitraire. Cependant, il ne peut gérer aucune entrée étant 0 à l'entrée (il détecte quand sub
produit un zéro, ce que x-0 ne fait jamais).
GNU C inline asm source pour la version 32 bits (compiler avec 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;
}
Normalement, j’écrirais toute une fonction dans asm, mais GNU C inline asm semble être le meilleur moyen d’inclure un extrait de code pouvant avoir des entrées / sorties dans toutes les règles de notre choix. Comme vous pouvez le constater, la syntaxe asm en ligne de GNU C rend asm laid et bruyant. C'est aussi un moyen très difficile d' apprendre asm .
Cela compilerait et fonctionnerait en .att_syntax noprefix
mode, parce que toutes les insns utilisées sont simples ou sans opérande ou xchg
. Ce n'est pas vraiment une observation utile.