Очень просто, что такое оптимизация хвостового вызова?
В частности, что это за небольшие фрагменты кода, где их можно применить, а где нет, с объяснением почему?
Очень просто, что такое оптимизация хвостового вызова?
В частности, что это за небольшие фрагменты кода, где их можно применить, а где нет, с объяснением почему?
Ответы:
При оптимизации хвостового вызова вы можете избежать выделения нового стекового фрейма для функции, потому что вызывающая функция просто вернет значение, которое она получает из вызываемой функции. Чаще всего используется хвостовая рекурсия, где рекурсивная функция, написанная для использования преимуществ оптимизации хвостовых вызовов, может использовать пространство постоянного стека.
Scheme является одним из немногих языков программирования, которые в спецификации гарантируют, что любая реализация должна обеспечивать эту оптимизацию (JavaScript также делает, начиная с ES6) , поэтому вот два примера функции факториала в Scheme:
Первая функция не является хвостовой рекурсивной, потому что, когда выполняется рекурсивный вызов, функция должна отслеживать умножение, которое необходимо выполнить с результатом после возврата вызова. Таким образом, стек выглядит следующим образом:
Напротив, трассировка стека для хвостового рекурсивного факториала выглядит следующим образом:
Как видите, нам нужно отслеживать только один и тот же объем данных для каждого вызова факт-хвоста, потому что мы просто возвращаем значение, которое получаем до самого верха. Это означает, что даже если бы мне пришлось звонить (факт 1000000), мне нужно только столько же места, сколько (факт 3). Это не относится к нерекурсивному факту, и такие большие значения могут вызвать переполнение стека.
источник
Давайте рассмотрим простой пример: функция факториала, реализованная в C.
Начнем с очевидного рекурсивного определения
Функция заканчивается хвостовым вызовом, если последняя операция перед возвратом функции - это другой вызов функции. Если этот вызов вызывает ту же функцию, она является хвостовой рекурсивной.
Хотя
fac()
на первый взгляд выглядит рекурсивным, это не так, как на самом делет.е. последняя операция - это умножение, а не вызов функции.
Однако можно переписать
fac()
его как хвостовую рекурсию, передав накопленное значение по цепочке вызовов в качестве дополнительного аргумента и передавая только конечный результат снова в качестве возвращаемого значения:Теперь, почему это полезно? Поскольку мы немедленно возвращаемся после хвостового вызова, мы можем отказаться от предыдущего стекового фрейма перед вызовом функции в хвостовой позиции или, в случае рекурсивных функций, повторно использовать стековый фрейм как есть.
Оптимизация хвостового вызова превращает наш рекурсивный код в
Это может быть включено в,
fac()
и мы приходим кчто эквивалентно
Как мы видим здесь, достаточно продвинутый оптимизатор может заменить хвостовую рекурсию итерацией, что гораздо более эффективно, поскольку вы избегаете накладных расходов на вызов функции и используете только постоянный объем стекового пространства.
источник
TCO (Tail Call Optimization) - это процесс, с помощью которого умный компилятор может выполнять вызов функции и не занимать дополнительное пространство в стеке. Единственная ситуация , в которой это происходит, если последняя команда выполняется в функции F является вызовом функции г (Примечание: г может быть е ). Ключевым моментом здесь является то, что для f больше не требуется место в стеке - он просто вызывает g и затем возвращает то, что вернул бы g . В этом случае можно сделать оптимизацию, так как g просто запускает и возвращает любое значение, которое он будет иметь, для вещи, которая называется f.
Эта оптимизация может заставить рекурсивные вызовы занимать постоянное место в стеке, а не взрываться.
Пример: эта факториальная функция не TCOptimizable:
Эта функция делает что-то кроме вызова другой функции в своем операторе возврата.
Эта функция ниже TCOptimizable:
Это потому, что последнее, что должно произойти в любой из этих функций, - это вызвать другую функцию.
источник
(cons a (foo b))
или(+ c (bar d))
в хвостовой позиции таким же образом.Вероятно, лучшее описание высокого уровня, которое я нашел для оконечных вызовов, рекурсивных оконечных вызовов и оптимизации оконечных вызовов, - это сообщение в блоге.
«Что, черт возьми, это: зов хвоста»
Дэн Сугальски. По оптимизации хвостового вызова он пишет:
И на хвостовой рекурсии:
Так что это:
тихо превращается в:
Что мне нравится в этом описании, так это то, насколько оно лаконично и легко понять для тех, кто пришел из императивного языка (C, C ++, Java)
источник
foo
оптимизирован ли начальный хвостовой вызов функции? Он просто вызывает функцию как последний шаг и просто возвращает это значение, верно?Прежде всего, обратите внимание, что не все языки поддерживают это.
TCO применяется к особому случаю рекурсии. Суть этого в том, что если последнее, что вы делаете в функции, это сам вызов (например, он вызывает себя из позиции «хвоста»), то компилятор может оптимизировать его так, чтобы он действовал как итерация, а не как стандартная рекурсия.
Вы видите, что обычно во время рекурсии среда выполнения должна отслеживать все рекурсивные вызовы, чтобы при возврате он мог возобновиться при предыдущем вызове и так далее. (Попробуйте вручную выписать результат рекурсивного вызова, чтобы получить наглядное представление о том, как это работает.) Отслеживание всех вызовов занимает место, которое становится значительным, когда функция сама вызывает много. Но с TCO можно просто сказать: «вернитесь к началу, только на этот раз измените значения параметров на эти новые». Это может быть сделано, потому что ничто после рекурсивного вызова не ссылается на эти значения.
источник
foo
оптимизирован ли начальный хвостовой метод?Пример минимального запуска GCC с анализом разборки x86
Давайте посмотрим, как GCC может автоматически выполнять оптимизацию хвостовых вызовов для нас, посмотрев на сгенерированную сборку.
Это послужит чрезвычайно конкретным примером того, что было упомянуто в других ответах, таких как https://stackoverflow.com/a/9814654/895245, что оптимизация может преобразовывать рекурсивные вызовы функций в цикл.
Это, в свою очередь, экономит память и повышает производительность, так как доступ к памяти часто является главной причиной замедления работы программ .
В качестве входных данных мы даем GCC неоптимизированный факториал на основе наивного стека:
tail_call.c
GitHub вверх по течению .
Скомпилируйте и разберите:
где
-foptimize-sibling-calls
имя обобщения хвостовых вызовов согласноman gcc
:как упомянуто в: Как я проверяю, выполняет ли gcc оптимизацию хвостовой рекурсии?
Я выбираю
-O1
потому что:-O0
. Я подозреваю, что это потому, что отсутствуют необходимые промежуточные преобразования.-O3
создает нечестиво эффективный код, который не будет очень познавательным, хотя он также оптимизирован с помощью хвостового вызова.Разборка с помощью
-fno-optimize-sibling-calls
:С
-foptimize-sibling-calls
:Основное различие между ними заключается в том, что:
то
-fno-optimize-sibling-calls
использованиеcallq
, что является типичным вызовом функции неоптимизированным.Эта инструкция помещает адрес возврата в стек, увеличивая его.
Кроме того, эта версия также делает
push %rbx
, что толкает%rbx
в стек .GCC делает это, потому что он сохраняет
edi
, который является первым аргументом функции (n
) вebx
, а затем вызываетfactorial
.GCC должен сделать это, потому что он готовится к другому вызову
factorial
, который будет использовать новыйedi == n-1
.Он выбирает,
ebx
потому что этот регистр сохранен вызываемым абонентом: какие регистры сохраняются посредством вызова функции linux x86-64, поэтому дополнительный вызовfactorial
не изменит его и не потеряетn
.-foptimize-sibling-calls
не использует какие - либо инструкций , которые толкают к стеке: он только делаетgoto
прыжки вfactorial
с инструкциямиje
иjne
.Следовательно, эта версия эквивалентна циклу while без каких-либо вызовов функций. Использование стека постоянно.
Протестировано в Ubuntu 18.10, GCC 8.2.
источник
Смотри сюда:
http://tratt.net/laurie/tech_articles/articles/tail_call_optimization
Как вы, вероятно, знаете, рекурсивные вызовы функций могут нанести ущерб стеку; легко быстро исчерпать пространство стека. Оптимизация вызова хвоста - это способ, с помощью которого вы можете создать алгоритм рекурсивного стиля, который использует постоянное пространство стека, поэтому он не растет и не растет, и вы получаете ошибки стека.
источник
Мы должны убедиться, что в самой функции нет операторов goto ... позаботились о том, чтобы вызов функции был последним в функции вызываемого.
Крупномасштабные рекурсии могут использовать это для оптимизации, но в небольшом масштабе накладные расходы на инструкции для вызова функции хвостовым вызовом уменьшают фактическую цель.
TCO может вызывать постоянно работающую функцию:
источник
У подхода с рекурсивной функцией есть проблема. Он создает стек вызовов размером O (n), что делает нашу общую стоимость памяти O (n). Это делает его уязвимым для ошибки переполнения стека, когда стек вызовов становится слишком большим и не хватает места.
Схема оптимизации Tail Call (TCO). Где он может оптимизировать рекурсивные функции, чтобы избежать создания большого стека вызовов и, следовательно, экономит стоимость памяти.
Есть много языков, которые делают TCO как (JavaScript, Ruby и немного C), тогда как Python и Java не делают TCO.
Язык JavaScript подтвержден с использованием :) http://2ality.com/2015/06/tail-call-optimization.html
источник
В функциональном языке оптимизация хвостового вызова - это как если бы вызов функции мог вернуть частично вычисленное выражение в качестве результата, который затем был бы оценен вызывающей стороной.
f 6 сводится к g 6. Поэтому, если реализация может вернуть g 6 в качестве результата, а затем вызвать это выражение, это сохранит кадр стека.
Также
Уменьшается до f 6 или до g 6 или h 6. Так что, если реализация оценивает c 6 и находит, что это правда, то она может уменьшиться,
Простой не хвостовой интерпретатор оптимизации вызовов может выглядеть так,
Интерпретатор оптимизации хвостового вызова может выглядеть так,
источник