На этот раз я хочу поделиться с вами одной проблемой, с которой я столкнулся, когда решил сравнить итеративные и рекурсивные функции в C++.
Между рекурсией и итерацией есть несколько различий: они подробно обсуждаются в этой статье.
В языках общего назначения, таких как Java, C или Python, рекурсия довольно дорога по сравнению с итерацией из-за необходимости каждый раз выделять новый кадр стека.
В C/C++ эти накладные расходы можно легко устранить, указав оптимизирующему компилятору использовать хвостовую рекурсию, которая преобразует определенные типы рекурсии (или, скорее, определенные типы хвостовых вызовов) в инструкции безусловного перехода.
Для реализации такого преобразования необходимо, чтобы самым последним действием функции перед возвратом значения был вызов другой функции (в данном случае самой себя).
В этом случае безусловный переход к началу второй подпрограммы безопасен.
Основным недостатком рекурсивных алгоритмов в императивных языках является то, что не всегда возможно иметь хвостовые вызовы, т.е.
выделение места для адреса функции (и связанных с ней переменных, таких как структуры) в стеке каждый раз при ее вызове.
.
Если глубина рекурсии слишком велика, это может привести к переполнению стека из-за ограничения на его максимальный размер, который обычно на порядки меньше объема оперативной памяти.
В качестве теста на изучение хвостовой рекурсии я написал простую функцию Фибоначчи на C++ в Visual Studio. Давайте посмотрим, как это работает:
Чтобы выполнить хвостовой вызов перед возвратом значения, функция фиб_хвост В качестве финального действия он вызывает себя.int fib_tail(int n, int res, int next) { if (n == 0) { return res; } return fib_tail(n - 1, next, res + next); }
Давайте теперь посмотрим на сгенерированный ассемблерный код. Для этого я скомпилировал программу в режиме выпуска, используя ключ оптимизации /O2. Вот как выглядит код:
Есть! Обратите внимание на последнюю строку: вместо инструкции ВЫЗОВ использовал СПМ .
В данном случае сработала хвостовая рекурсия, и у нашей функции не будет проблем с переполнением стека, поскольку на уровне ассемблера она превращена в итеративную функцию.
Мне этого оказалось недостаточно, и я решил поэкспериментировать с производительностью, увеличив входную переменную н .
Затем я изменил тип переменных, используемых в функции, с интервал на без подписи длинный длинный .
После повторного запуска программы у меня внезапно произошло переполнение стека! Вот как выглядит эта версия нашей функции: typedef unsigned long long ULONG64;
ULONG64 fib_tail(ULONG64 n, ULONG64 res, ULONG64 next)
{
if (n == 0)
{
return res;
}
return fib_tail(n - 1, next, res + next);
}
Давайте еще раз посмотрим на сгенерированный ассемблерный код:
Как я и ожидал, хвостовой рекурсии здесь не было! Теперь вместо ожидаемого СПМ использовал ВЫЗОВ .
Между тем, единственная разница между двумя версиями нашей функции заключается в том, что во втором случае я использовал 64-битную переменную вместо 32-битной.
Возникает вопрос: почему компилятор не использует хвостовую рекурсию при использовании 64-битных переменных?
Я решил скомпилировать программу в 64-битном режиме и посмотреть, как она себя поведет. Сгенерированный ассемблерный код:
Здесь снова появляется хвостовая рекурсия! Благодаря 64-битным регистрам (rax, r8, rcx, rdx) вызывающая и вызываемая функции используют общий стек, а вызываемая функция возвращает результат непосредственно в точку вызова внутри вызывающей функции.
Я задал свой вопрос на сайте Переполнение стека — похоже, проблема в самом компиляторе Microsoft C++.
Автор одного из комментариев сказал, что в других компиляторах C++ такой проблемы не наблюдается, но мне нужно убедиться в этом самому.
Я разместил пример кода на GitHub – вы можете скопировать его и попробовать запустить самостоятельно.
На Реддит и Stackoverflow также сообщил мне, что описанная проблема не возникает в VS2013 Community Edition. Я пробовал работать в VS2013 Ultimate, но и там столкнулся с проблемами.
В ближайшие несколько дней я постараюсь протестировать код под GCC и сравнить результаты.
См.
пример проекта на GitHub .
Надеюсь, мое расследование окажется для вас полезным, если вы также разберетесь, почему в определенных случаях компилятор не реализует хвостовую рекурсию.
До скорой встречи! Продолжение: habrahabr.ru/company/pvs-studio/blog/261029 Теги: #компиляторы #оптимизация кода #Ассемблер #64-бит #хвостовая рекурсия #x64 #64бит #64-бит #64-битное программирование
-
Настройка Безопасности Свойств Корзины
19 Oct, 24 -
Обзоры Продуктов: Internet Explorer
19 Oct, 24 -
Выпущен Linux Mint 9 «Айседора»
19 Oct, 24 -
Alphago Выиграла Вторую Игру У Ли Седоля
19 Oct, 24 -
Идея: Учить Иностранные Слова По Книгам
19 Oct, 24 -
Чего Боятся Программисты?
19 Oct, 24 -
Повесьте Мышку На Елку В Новый Год!
19 Oct, 24 -
Конкурс Приложений Tizen
19 Oct, 24