i386 (x86-32) машинный код, 8 байт (9B для без знака)
+1, если нам нужно обработать b = 0
ввод.
машинный код amd64 (x86-64), 9 байтов (10B для неподписанных или 14B 13B для 64-целых чисел со знаком или без знака)
10 9B для неподписанного на amd64, который разрывается при любом входе = 0
Входы 32bit ненулевые подписанные целые числа eax
и ecx
. Выход в 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
Эта структура цикла не проходит тестовый случай, где ecx = 0
. ( div
вызывает #DE
аппаратное выполнение при делении на ноль. (В Linux ядро выдает SIGFPE
(исключение с плавающей запятой).) Если бы точка входа в цикл была прямо перед inc
, мы бы избежали этой проблемы. Версия x86-64 может справиться с этим бесплатно смотрите ниже.
Ответ Майка Шланты был отправной точкой для этого . Мой цикл делает то же самое, что и его, но для целых чисел со cdq
знаком, потому что он на один байт короче xor edx,edx
. И да, он работает правильно с одним или обоими входами отрицательными. Версия Майка будет работать быстрее и занимать меньше места в кэше UOP ( xchg
на процессорах Intel это 3 мопа, а loop
на большинстве процессоров она очень медленная ), но эта версия выигрывает при размере машинного кода.
Сначала я не заметил, что вопрос требует 32-разрядного без знака . Возвращение к xor edx,edx
вместо cdq
будет стоить один байт. div
имеет такой же размер, как idiv
и все остальное, может оставаться неизменным ( xchg
для перемещения данных и при inc/loop
этом работы).
Интересно, что для 64-битного размера операнда ( rax
и rcx
) версии со знаком и без знака имеют одинаковый размер. Подписанная версия нуждается в префиксе REX для cqo
(2B), но неподписанная версия все еще может использовать 2B xor edx,edx
.
В 64-битном коде inc ecx
это 2B: однобайтовые inc r32
и dec r32
коды операций были переназначены как префиксы REX. inc/loop
не сохраняет размер кода в 64-битном режиме, так что вы тоже можете test/jnz
. Работа с 64-битными целыми числами добавляет еще один байт на инструкцию в префиксах REX, за исключением loop
или jnz
. Остальные могут иметь все нули в младшем 32b (например gcd((2^32), (2^32 + 1))
), поэтому мы должны протестировать весь rcx и не можем сохранить байт с помощью test ecx,ecx
. Однако более медленный jrcxz
insn составляет всего 2B, и мы можем поместить его в верхнюю часть цикла для обработки ecx=0
при входе :
## 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
Полная работоспособная программа испытаний , включающие в себя , main
что работает printf("...", gcd(atoi(argv[1]), atoi(argv[2])) );
источник и выход ASM на Godbolt Compiler проводнике , для 32 и 64b версий. Протестировано и работает для 32bit ( -m32
), 64bit ( -m64
) и x32 ABI ( -mx32
) .
Также включены: версия, использующая только повторное вычитание , которая составляет 9B для без знака, даже для режима x86-64, и может принимать один из своих входов в произвольном регистре. Тем не менее, он не может обрабатывать ни один из входов, sub
равный 0 на входе (он обнаруживает, когда выдает ноль, чего никогда не делает x - 0).
Встроенный источник GNU C для 32-битной версии (скомпилировать с 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;
}
Обычно я пишу целую функцию в asm, но встроенный asm в GNU C кажется лучшим способом включить фрагмент, который может иметь входы / выходы в любых регистрах, которые мы выберем. Как вы можете видеть, встроенный синтаксис GNU C делает asmd уродливым и шумным. Это также очень сложный способ изучения асма .
На самом деле он будет компилироваться и работать в .att_syntax noprefix
режиме, потому что все используемые insns являются либо одиночным / без операнда, либо xchg
. Не очень полезное наблюдение.