i386 (x86-32) код машини, 8 байт (9B для неподписаного)
+ 1B, якщо нам потрібно обробляти b = 0
на вході.
машинний код amd64 (x86-64), 9 байт (10B для непідписаних чи 14B 13B для 64b цілих чисел, підписаних або непідписаних)
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
. І так, він працює правильно з одним або обома вхідними мінусами. Версія Майка працюватиме швидше і займе менше місця в загальному кеші ( xchg
3 процесори на процесорах Intel і 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
інс становить лише 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. Тестували та працювали для 32-бітових ( -m32
), 64-бітових ( -m64
) та x32 ABI ( -mx32
) .
Також включена: версія, що використовує лише повторне віднімання , яка становить 9В для непідписаного, навіть для режиму x86-64, і може приймати один із своїх входів у довільному регістрі. Однак він не може обробити жоден вхід 0 при вході (він виявляє, коли sub
виробляє нуль, що x - 0 ніколи не робить).
Вбудоване джерело ASM 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, але GNU C вбудований asm, здається, є найкращим способом включити фрагмент, який може мати вхід / вихід у будь-які регіони, які ми виберемо. Як бачите, GNU C вбудований синтаксис asm робить asm некрасивим і галасливим. Це також дійсно складний спосіб засвоїти асм .
Він насправді .att_syntax noprefix
збирається і працює в режимі, тому що всі використовувані записи є одиничними / без операнду або xchg
. Не дуже корисне спостереження.