código da máquina i386 (x86-32), 8 bytes (9B para não assinado)
+ 1B se precisarmos lidar b = 0com a entrada.
código de máquina amd64 (x86-64), 9 bytes (10B para não assinado ou 14B 13B para números inteiros de 64b assinados ou não)
10 9B para não assinado em amd64 que quebra com a entrada = 0
As entradas são de 32 bits zero não assinados inteiros eaxe ecx. Saída em 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
Essa estrutura de loop falha no caso de teste em que ecx = 0. ( divcausa uma #DEexecução de hardware na divisão por zero. (No Linux, o kernel fornece uma SIGFPE(exceção de ponto flutuante)). Se o ponto de entrada do loop estivesse logo antes do inc, evitaríamos o problema. A versão x86-64 pode lidar com isso gratuitamente, veja abaixo.
A resposta de Mike Shlanta foi o ponto de partida para isso . Meu loop faz a mesma coisa que o dele, mas para números inteiros assinados porque cdqé um byter menor que xor edx,edx. E sim, ele funciona corretamente com uma ou ambas as entradas negativas. A versão de Mike será mais rápida e ocupará menos espaço no cache uop ( xchgsão 3 uops nas CPUs Intel e loopé realmente lenta na maioria das CPUs ), mas essa versão vence no tamanho de código de máquina.
Inicialmente, não percebi que a pergunta exigia 32 bits não assinados . Voltar para em xor edx,edxvez de cdqcustaria um byte. divé do mesmo tamanho idive tudo o mais pode permanecer o mesmo ( xchgpara movimentação de dados e inc/loopainda funciona).
Curiosamente, para operandos de 64 bits ( raxe rcx), as versões assinadas e não assinadas têm o mesmo tamanho. A versão assinada precisa de um prefixo REX para cqo(2B), mas a versão não assinada ainda pode usar 2B xor edx,edx.
No código de 64 bits, inc ecxé 2B: o byte único inc r32e os dec r32códigos de operação foram redirecionados como prefixos REX. inc/loopnão salva nenhum tamanho de código no modo de 64 bits, então você também pode test/jnz. Operar em números inteiros de 64 bits adiciona outro byte por instrução nos prefixos REX, exceto para loopou jnz. É possível que o restante tenha todos os zeros em 32b baixo (por exemplo gcd((2^32), (2^32 + 1))), portanto, precisamos testar todo o rcx e não podemos salvar um byte test ecx,ecx. No entanto, o jrcxzinsn mais lento é apenas 2B, e podemos colocá-lo no topo do loop para lidar ecx=0com a entrada :
## 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
Programa de teste executável completo, incluindo um mainque executa printf("...", gcd(atoi(argv[1]), atoi(argv[2])) ); saída de origem e asm no Godbolt Compiler Explorer , para as versões 32 e 64b. Testado e funcionando para 32 bits ( -m32), 64 bits ( -m64) e x32 ABI ( -mx32) .
Também incluído: uma versão usando apenas subtração repetida , que é 9B para o sinal não assinado, mesmo para o modo x86-64, e pode receber uma de suas entradas em um registro arbitrário. No entanto, ele não pode manipular nenhuma entrada sendo 0 na entrada (ele detecta quando subproduz um zero, o que x - 0 nunca faz).
Fonte em linha GNU C asm para a versão de 32 bits (compilação com 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 eu escreveria uma função inteira no asm, mas o GNU C inline asm parece ser a melhor maneira de incluir um trecho que pode ter in / outputs em quaisquer regs que escolhermos. Como você pode ver, a sintaxe do GNU C inline asm torna asm feia e barulhenta. Também é uma maneira realmente difícil de aprender asm .
Na verdade, ele compilaria e funcionaria no .att_syntax noprefixmodo, porque todos os insns usados são únicos / sem operando ou xchg. Não é realmente uma observação útil.