Почему .NET / C # не оптимизирован для рекурсии хвостового вызова?

Я нашел этот вопрос о том, какие языки оптимизируют хвостовую рекурсию. Почему C # не оптимизирует хвостовую рекурсию, когда это возможно?

В конкретном случае, почему этот метод не оптимизирован в цикл (Visual Studio 2008 32-битный, если это важно) ?:

private static void Foo(int i)
{
    if (i == 1000000)
        return;

    if (i % 100 == 0)
        Console.WriteLine(i);

    Foo(i+1);
}

person ripper234    schedule 29.01.2009    source источник
comment
Сегодня я читал книгу о структурах данных, в которой рекурсивная функция раздваивается на две, а именно preemptive (например, факториальный алгоритм) и Non-preemptive (например, функция Аккермана). Автор привел всего два примера, которые я привел, не объясняя эту раздвоение должным образом. Является ли эта бифуркация такой же, как хвостовые и нехвостовые рекурсивные функции?   -  person RBT    schedule 23.09.2016
comment
Полезный разговор об этом Джона Скита и Скотта Хансельмана в youtu.be/H2KkiRbDZyc?t=3302   -  person Daniel B    schedule 19.09.2017
comment
@RBT: Я думаю, что это другое. Это относится к количеству рекурсивных вызовов. Хвостовые вызовы - это вызовы, которые появляются в хвостовой позиции, то есть последнее, что делает функция, так как она возвращает результат напрямую от вызываемого.   -  person J D    schedule 13.12.2018


Ответы (6)


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

Интересно, что шаги компиляции NGen не нацелены на более агрессивную оптимизацию. Я подозреваю, что это потому, что они просто не хотят иметь ошибок, поведение которых зависит от того, отвечает ли JIT или NGen за машинный код.

Сам CLR поддерживает оптимизацию хвостового вызова, но компилятор для конкретного языка должен знать, как создавать соответствующий opcode, и JIT должна уважать его. F # fsc сгенерирует соответствующие коды операций (хотя для простой рекурсии он может просто преобразовать все это напрямую в цикл while). C # C # нет.

См. это сообщение в блоге для получения некоторых подробностей (вполне возможно, что сейчас нет дата с учетом последних изменений JIT). Обратите внимание, что среда CLR изменяется для 4.0 x86, x64 и ia64 будут уважать его.

person ShuggyCoUk    schedule 29.01.2009
comment
См. Также этот пост: social.msdn.microsoft.com/Forums/en-US/netfxtoolsdev/thread/, при этом я обнаружил, что хвост работает медленнее, чем обычный вызов. Ээп! - person plinth; 29.01.2009

Это Отправка отзыва на Microsoft Connect должна ответить на ваш вопрос. Он содержит официальный ответ от Microsoft, поэтому я бы рекомендовал пойти по нему.

Спасибо за предложение. Мы рассмотрели создание инструкций хвостового вызова на нескольких этапах разработки компилятора C #. Однако есть некоторые тонкие проблемы, которые заставили нас избегать этого до сих пор: 1) На самом деле существует нетривиальные накладные расходы на использование инструкции .tail в CLR (это не просто инструкция перехода, поскольку хвостовые вызовы в конечном итоге становятся во многих менее строгих средах, таких как среды выполнения функционального языка, где хвостовые вызовы сильно оптимизированы). 2) Существует несколько реальных методов C #, в которых было бы законно генерировать хвостовые вызовы (другие языки поощряют шаблоны кодирования, которые имеют больше хвостовой рекурсии, и многие, которые в значительной степени полагаются на оптимизацию хвостовых вызовов, фактически выполняют глобальную перезапись (например, преобразования с продолжением передачи) ) для увеличения количества хвостовой рекурсии). 3) Отчасти из-за 2) случаи переполнения стека методов C # из-за глубокой рекурсии, которая должна была пройти, довольно редки.

С учетом всего сказанного, мы продолжаем изучать это, и мы можем в будущем выпуске компилятора найти некоторые шаблоны, в которых имеет смысл генерировать инструкции .tail.

Кстати, как уже указывалось, стоит отметить, что хвостовая рекурсия оптимизирована для x64.

person Noldorin    schedule 29.01.2009
comment
Вы также можете найти это полезным: weblogs.asp.net/podwysocki/archive/2008/07/07/ - person Noldorin; 29.01.2009
comment
Без проблем, рад, что ты нашел это полезным. - person Noldorin; 29.01.2009
comment
Спасибо, что процитировали это, потому что теперь это 404! - person Roman Starkov; 21.11.2012
comment
Ссылка теперь исправлена. - person luksan; 29.10.2013

C # не оптимизирован для рекурсии хвостового вызова, потому что F # для этого предназначен!

Более подробно об условиях, которые не позволяют компилятору C # выполнять оптимизацию хвостового вызова, см. В этой статье: JIT CLR tail-call conditions.

Взаимодействие между C # и F #

C # и F # взаимодействуют очень хорошо, и поскольку среда .NET Common Language Runtime (CLR) разработана с учетом этой возможности взаимодействия, каждый язык разработан с оптимизацией, специфичной для его целей и задач. Пример, показывающий, насколько легко вызвать код F # из кода C #, см. В разделе Вызов F # код из кода C #; для примера вызова функций C # из кода F # см. Вызов функций C # из F #.

Для делегирования взаимодействия см. Эту статью: Делегирование взаимодействия между F #, C # и Visual Basic.

Теоретические и практические различия между C # и F #

Вот статья, в которой рассматриваются некоторые различия и объясняются конструктивные различия рекурсии хвостового вызова между C # и F #: Создание кода операции Tail-Call на C # и F #.

Вот статья с некоторыми примерами на C #, F # и C ++ \ CLI: Приключения в хвостовой рекурсии в C #, F # и C ++ \ CLI

Основное теоретическое отличие состоит в том, что C # разработан с использованием циклов, тогда как F # разработан на принципах лямбда-исчисления. Очень хорошую книгу по принципам лямбда-исчисления см. В этой бесплатной книге: Структура и интерпретация компьютерных программ, автор Абельсон , Сассман и Сассман.

Очень хорошую вводную статью о хвостовых вызовах в F # см. В этой статье: Подробное введение в хвостовые вызовы в F #. Наконец, вот статья, в которой рассматривается разница между рекурсией без хвостовой части и рекурсией хвостового вызова (в F #): Хвостовая рекурсия против не-хвостовой рекурсии в фа-диез.

person devinbost    schedule 28.04.2014

Мне недавно сказали, что компилятор C # для 64-битной версии оптимизирует хвостовую рекурсию.

C # также реализует это. Причина, по которой это не всегда применяется, заключается в том, что правила, используемые для применения хвостовой рекурсии, очень строгие.

person Alexandre Brisebois    schedule 29.01.2009
comment
X64 jitter делает это, но компилятор C # не делает этого. - person Mark Sowul; 23.09.2011
comment
Спасибо за информацию. Этот белый цвет отличается от того, что я думал ранее. - person Alexandre Brisebois; 27.09.2011
comment
Чтобы прояснить эти два комментария, C # никогда не испускает код операции CIL 'tail', и я считаю, что это все еще верно в 2017 году. Однако для всех языков этот код операции всегда является рекомендательным только в том смысле, что соответствующие джиттеры (x86, x64) будут молча игнорировать его, если различные условия не выполняются (ну, никакой ошибки, кроме возможного переполнения стека). Это объясняет, почему вы вынуждены следовать за «хвостом» с помощью «ret» - это для этого случая. Между тем, джиттеры также могут применить оптимизацию, когда в CIL нет префикса «хвоста», опять же, если это считается целесообразным, и независимо от языка .NET. - person Glenn Slayden; 01.10.2017

Для хвостовых рекурсивных функций в C # ( или Java). Однако лучшим решением (если вы просто заботитесь об использовании стека) является использование этот небольшой вспомогательный метод, чтобы обернуть части одной и той же рекурсивной функции и сделать ее итеративной, сохраняя при этом функцию читаемой.

person naiem    schedule 23.05.2012
comment
Батуты агрессивны (они представляют собой глобальное изменение соглашения о вызовах), примерно в 10 раз медленнее, чем надлежащее исключение хвостовых вызовов, и они скрывают всю информацию трассировки стека, что значительно затрудняет отладку и профилирование кода - person J D; 13.12.2018

Как упоминалось в других ответах, CLR поддерживает оптимизацию хвостового вызова, и, похоже, исторически происходили прогрессивные улучшения. Но поддержка его на C # имеет открытую Proposal проблему в репозитории git для разработки языка программирования C # Поддержка хвостовой рекурсии № 2544.

Здесь вы можете найти полезные подробности и информацию. Например, @jaykrell упомянул

Позвольте мне поделиться тем, что я знаю.

Иногда обратный вызов - беспроигрышный результат. Это может сэкономить CPU. jmp дешевле, чем call / ret. Он может сэкономить стек. Прикосновение к меньшему количеству стопки обеспечивает лучшую локацию.

Иногда обратный вызов - это потеря производительности, выигрыш стека. В CLR есть сложный механизм, в котором вызываемой стороне передается больше параметров, чем получено вызывающей стороной. Я имею в виду, в частности, больше места в стеке для параметров. Это медленно. Но это сохраняет стек. Он будет делать это только с хвостом. префикс.

Если параметры вызывающего объекта больше, чем параметры вызываемого объекта, это обычно довольно простое беспроигрышное преобразование. Могут быть факторы, такие как изменение позиции параметра с управляемого на целое число / число с плавающей запятой, создание точных карт StackMaps и т. Д.

Теперь есть еще один аспект - алгоритмы, которые требуют устранения хвостовых вызовов, чтобы иметь возможность обрабатывать произвольно большие данные с фиксированным / малым стеком. Дело не в производительности, а в способности бегать вообще.

Также позвольте мне упомянуть (в качестве дополнительной информации), когда мы генерируем скомпилированную лямбду с использованием классов выражений в пространстве имен System.Linq.Expressions, есть аргумент с именем 'tailCall', который, как объяснено в его комментарии,

Логическое значение, указывающее, будет ли применяться оптимизация хвостового вызова при компиляции созданного выражения.

Я еще не пробовал это, и я не уверен, как это может помочь в связи с вашим вопросом, но, вероятно, кто-то может попробовать это и может быть полезно в некоторых сценариях:


var myFuncExpression = System.Linq.Expressions.Expression.Lambda<Func< … >>(body: … , tailCall: true, parameters: … );

var myFunc =  myFuncExpression.Compile();

person lifestyle    schedule 21.12.2019