Helpers - компьютеры, интернет, программирование

Что делает gcc -O2 с рекурсивной функцией Фибоначчи?

Я изучаю ассемблер 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 временных int. Затем edi читается из [esp+32], который является аргументом. Если аргумент <=1, то он переходит к L4, который обнуляет (?) esi перед тем, как вернуться к L2, который устанавливает eax=esi+edi, который является просто аргументом edi. Если аргумент был >1, то аргумент копируется в ebx и обнуляет esi перед тем, как попасть в L3. В L3 он устанавливает eax=ebx-1 и сохраняет результат (n-1) в esp в кадре стека перед повторным выполнением вычисления fib(n-1). Результат добавляется к esi, ebx устанавливается на n-2 и возвращается к L3, если ebx>1, в противном случае он извлекает младший бит edi перед тем, как перейти к L2.

Почему этот код такой запутанный (например, есть ли название для проделанной оптимизации, которое я не вижу?)?

Рекурсивные вызовы fib(n-2), похоже, были заменены циклом, накапливающимся в esi, но этот вызов не был в хвостовой позиции, так как же это было сделано?

Для чего нужен and edi, 1?

Какова цель mov DWORD PTR [esp], eax?

Почему фрейм стека такой большой?

Можете ли вы разобрать этот алгоритм обратно на C, чтобы прояснить, что происходит?

У меня предварительное впечатление, что GCC генерирует довольно плохой ассемблер x86. В этом случае более чем в 2 раза больше кода для одинаковой производительности (3,25 с для fib (40) на этом 1,6 ГГц Atom для обеих программ). Это честно?

07.04.2012


Ответы:


1

В дополнение к комментариям выше обратите внимание, что рекурсия была развернута в хвостовой вызов путем замены:

return x < 2 ? x : fib(x - 2) + fib(x - 1);

с участием:

if ((xprime = x) < 2) {
    acc = 0;
} else {
    /* at this point we know x >= 2 */
    acc = 0; /* start with 0 */
    while (x > 1) {
       acc += fib(x - 1); /* add fib(x-1) */
       x -= 2; /* now we'll add fib(x-2) */
    }
    /* so at this point we know either x==1 or x==0 */
    xprime = x == 1 ? 1 : 0; /* ie, x & 1 */
}
return xprime + acc;

Я подозреваю, что этот довольно сложный цикл возник из-за нескольких шагов оптимизации, а не потому, что я возился с оптимизацией gcc примерно с gcc 2.3 (теперь все внутри совсем другое!).

07.04.2012

2

Проще говоря, fib(x-2) равно fib(x-3) + fib(x-4), fib(x-4) равно fib(x-5) + fib(x-6) и т. Д., Поэтому fib(x) вычисляется как fib(x-1) + fib(x-3) + fib(x-5) + ... + fib(x&1) (fib(x&1) равно x&1), т.е. gcc встроил вызов fib(x-2), что довольно умно для рекурсивной функции.

07.04.2012

3

Эта первая часть гарантирует, что регистры, которые должны быть сохранены в соответствии с соглашением о вызовах, не удаляются. Я предполагаю, что здесь используется соглашение о вызовах cdecl.

_fib:
    push    edi
    push    esi
    push    ebx
    sub esp, 16

DWORD PTR[esp+32] это ваш x:

    mov edi, DWORD PTR [esp+32]
    cmp edi, 1
    jle L4

Если x меньше или равно 1 (это соответствует вашему x < 2), перейдите к L4, который:

L4:
    xor esi, esi
    jmp L2

Это обнуляет esi и переходит к L2:

L2:
    lea eax, [esi+edi]
    add esp, 16
    pop ebx
    pop esi
    pop edi
    ret

Это устанавливает eax (возвращаемое значение) в esi+edi. Поскольку esi уже равен 0, edi загружается только в случае 0 и 1. Это соответствует x < 2 ? x.

Теперь рассмотрим случай, когда x не < 2:

    mov ebx, edi
    xor esi, esi
L3:
    lea eax, [ebx-1]
    mov DWORD PTR [esp], eax
    call    _fib

Сначала x копируется в ebx, затем esi обнуляется.

Затем eax устанавливается на x - 1. Это значение перемещается в верхнюю часть стека и вызывается _fib. Это соответствует fib(x-1).

    sub ebx, 2
    add esi, eax

Это вычитает 2 из ebx (x). Затем eax (возвращаемое значение из вызова _fib добавляется к esi, которое раньше было установлено в 0). Следовательно, esi теперь содержит результат fib(x-1).

    cmp ebx, 1
    jg  L3
    and edi, 1

ebx сравнивается с 1. Если он больше 1, мы возвращаемся к L3. В противном случае (случай, когда это 0 или 1), мы выполняем and edi, 1 и переходим к L2 (мы уже проанализировали, что это делает ранее). and edi, 1 эквивалентно выполнению %2 на x.

Вот что делает код на высоком уровне:

  • Устанавливает стековый фрейм и сохраняет регистры
  • Если x < 2, вернуть x.
  • Продолжайте набирать и суммировать fib(x-...), пока x не станет меньше 2. В этом случае переходите к случаю x < 2.

Оптимизация состоит в том, что GCC разворачивает случаи, когда x >= 2, выполняя их в цикле, а не рекурсивно.

07.04.2012
Новые материалы

Интуитивное понимание тензоров в машинном обучении
Тензор является важной концепцией во многих научных областях, таких как математика, физика, обработка сигналов и компьютерное зрение, и это лишь некоторые из них. В математике тензор — это..

Использование машинного обучения для диагностики болезни Альцгеймера, часть 4
Маркеры семантической согласованности для ранней диагностики болезни Альцгеймера (arXiv) Автор: Давиде Колла , Маттео Дельсанто , Марко Агосто , Бенедетто Витиелло , Даниэле Паоло Радичони..

Почему объяснимость так важна прямо сейчас?
По мере того, как системы искусственного интеллекта и инструменты на основе машинного обучения распространяются в нашей повседневной жизни, как практики, так и критики все чаще заявляют о..

Анимированный математический анализ
Использование Manim для создания математических анимированных визуализаций Визуализация данных помогает понять скрытые закономерности в данных, которые невозможно визуализировать..

Создание простого слайдера изображений с помощью JavaScript
Узнайте, как создать базовый слайдер изображений с помощью HTML, CSS и JavaScript. Введение В этом уроке мы создадим удобный слайдер изображений, используя JavaScript, HTML и CSS. Ползунок..

Создание базы данных с помощью супергероя «Python»
В этом посте мы узнаем, как создать «базу данных SQLite с помощью модуля python sqlite3, создав простую функцию входа и регистрации. Готовы ли вы к этому путешествию? Если да , давайте приступим..

ИИ для чайников: руководство для начинающих по пониманию будущего технологий
Вы чувствуете, что остались позади в мире ИИ? Не волнуйтесь, вы не одиноки! Со всей этой шумихой вокруг искусственного интеллекта может быть трудно понять, с чего начать. Но не позволяйте сленгу..