código da máquina i386 (x86-32), 8 bytes (9B para não assinado)
+ 1B se precisarmos lidar b = 0
com 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 eax
e 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
. ( div
causa uma #DE
execuçã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 ( xchg
sã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,edx
vez de cdq
custaria um byte. div
é do mesmo tamanho idiv
e tudo o mais pode permanecer o mesmo ( xchg
para movimentação de dados e inc/loop
ainda funciona).
Curiosamente, para operandos de 64 bits ( rax
e 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 r32
e os dec r32
códigos de operação foram redirecionados como prefixos REX. inc/loop
nã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 loop
ou 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 jrcxz
insn mais lento é apenas 2B, e podemos colocá-lo no topo do loop para lidar ecx=0
com 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 main
que 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 sub
produz 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 noprefix
modo, porque todos os insns usados são únicos / sem operando ou xchg
. Não é realmente uma observação útil.