Я изучаю ассемблер x86, чтобы написать компилятор. В частности, я беру множество простых рекурсивных функций и прогоняю их через разные компиляторы (OCaml, GCC и т. д.), чтобы лучше понять виды ассемблера, генерируемые разными компиляторами.
У меня есть тривиальная рекурсивная целочисленная функция Фибоначчи:
int fib(int x) { return (x < 2 ? x : fib(x-1)+fib(x-2)); }
Моя ручная сборка выглядит следующим образом:
fib:
cmp eax, 2
jl fin
push eax
dec eax
call fib
push eax
mov eax, [esp+4]
add eax, -2
call fib
add eax, [esp]
add esp, 8
fin:
ret
Компиляция этой функции в Intel-синтаксический ассемблер с использованием gcc -O2
дает вот это загадочное код:
_fib:
push edi
push esi
push ebx
sub esp, 16
mov edi, DWORD PTR [esp+32]
cmp edi, 1
jle L4
mov ebx, edi
xor esi, esi
L3:
lea eax, [ebx-1]
mov DWORD PTR [esp], eax
call _fib
sub ebx, 2
add esi, eax
cmp ebx, 1
jg L3
and edi, 1
L2:
lea eax, [esi+edi]
add esp, 16
pop ebx
pop esi
pop edi
ret
L4:
xor esi, esi
jmp L2
Итак, я предполагаю, что соглашение о вызовах - это аргумент в [esp+4]
и возвращаемое значение в eax
.Он начинается с нажатия edi
, esi
и ebx
. Затем он требует еще 16 байтов для кадра стека, что достаточно для 4 временных целых чисел. Затем edi
читается из [esp+32]
, что является аргументом. Если аргумент равен <=1
, то выполняется переход к L4
, который обнуляет (?) esi
перед возвратом к L2
, который устанавливает eax=esi+edi
который является просто аргументом edi
. Если аргумент был >1
, то аргумент копируется в ebx
и обнуляется esi
перед попаданием в L3
. В L3
он устанавливает eax=ebx-1
и сохраняет результат (n-1) в esp
в кадре стека перед рекурсивным вычислением выдумка(n-1)
. Результат добавляется к esi
, ebx
устанавливается в n-2
и возвращается к L3
, если ebx> 1
, в противном случае он извлекает младший бит из edi
перед переходом к L2
.
Почему этот код такой запутанный (например,есть ли название выполненной оптимизации, которую я не вижу?)?
Рекурсивные вызовы fib(n-2)
, по-видимому, были заменены циклом, накапливающимся в esi
, но этот вызов не был в хвостовой позиции, так как же это было сделано?
Какова цель и edi, 1
?
Какова цель mov DWORD PTR [esp], eax
?
Почему кадр стека такой большой?
Можете ли вы разобрать этот алгоритм обратно на C, чтобы было понятнее, что происходит?
Мое предварительное впечатление, что GCC генерирует довольно плохой ассемблер x86. В этом случае более чем в 2 раза больше кода для одинаковой производительности (3,25 с для fib(40) на этом 1,6 ГГц Atom для обеих программ). Это честно?