Оптимизация вызовов на C - PullRequest
       41

Оптимизация вызовов на C

30 голосов
/ 18 августа 2010

Я часто слышу, как люди говорят, что C не выполняет удаление хвостовых вызовов. Хотя это не гарантируется стандартом, разве это не выполняется на практике какой-либо достойной реализацией? Предполагая, что вы нацелены только на зрелые, хорошо реализованные компиляторы и не заботитесь об абсолютной максимальной переносимости к примитивным компиляторам, написанным для неясных платформ, разумно ли полагаться на исключение хвостовых вызовов в C?

Кроме того, каково было основание для того, чтобы исключить оптимизацию хвостового вызова из стандарта?

Ответы [ 8 ]

27 голосов
/ 18 августа 2010

Утверждения типа «C не выполняет удаление хвостовых вызовов» не имеют смысла. Как вы правильно заметили, подобные вещи полностью зависят от реализации. И да, любая достойная реализация может легко превратить хвостовую рекурсию в [эквивалент] цикла. Конечно, компиляторы C, как правило, не дают никаких гарантий относительно того, какие оптимизации будут и какие оптимизации не будут выполняться в каждом конкретном фрагменте кода. Вы должны скомпилировать его и убедиться сами.

6 голосов
/ 01 мая 2014

Хотя современные компиляторы МОГУТ выполнять оптимизацию хвостовых вызовов, если вы включите оптимизацию, ваши отладочные сборки, вероятно, будут работать без нее, так что вы сможете получать трассировки стека и входить / выходить из кода и таких замечательных вещей. В этой ситуации оптимизация хвостового вызова нежелательна.

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

5 голосов
/ 18 августа 2010

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

Язык программирования C (IMHO) явно был , а не , предназначенным для функционального программирования. Существуют все виды циклических конструкций, которые обычно используются в пользу рекурсии: for, do .. while, while. На таком языке было бы бессмысленно предписывать оптимизацию хвостовых вызовов в стандарте, поскольку не требуется строго гарантировать работающие программы.

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


P.S .: Обратите внимание на небольшую ошибку в моем аргументе для оптимизации хвостового вызова. В конце я упоминаю переполнение стека. Но кто сказал, что вызовы функций всегда требуют стека? На некоторых платформах вызовы функций могут быть реализованы совершенно по-другому, и переполнение стека никогда не будет проблемой. Это было бы еще одним аргументом против предписания оптимизации хвостового вызова в стандарте. (Но не поймите меня неправильно, я вижу преимущества такой оптимизации даже без стека!)

3 голосов
/ 18 августа 2010

Чтобы ответить на последний вопрос: стандарт определенно не должен делать никаких заявлений об оптимизации.Могут быть среды, в которых это более или менее сложно реализовать.

1 голос
/ 17 февраля 2018

Существуют ситуации, когда оптимизация хвостового вызова потенциально может нарушить ABI или, по крайней мере, будет очень трудно реализовать семантически сохраняющим способом. Например, подумайте о позиционно-независимом коде в общих библиотеках: некоторые платформы позволяют программам динамически связываться с библиотеками, чтобы сэкономить основную память, когда различные приложения зависят от одной и той же функциональности. В таких случаях библиотека загружается один раз и отображается в каждой виртуальной памяти программы, как если бы она была единственным приложением в системе. В UNIX, а также в некоторых других системах это достигается путем использования независимого от позиции кода для библиотек, чтобы адресация относилась к смещению, а не к фиксированному адресному пространству. Однако на многих платформах код, не зависящий от позиции, не должен быть оптимизирован с помощью хвостового вызова. Проблема заключается в том, что смещения для навигации по программе должны храниться в регистрах; на 32-битном Intel используется %ebx, который является регистром сохраненных вызовов; другие платформы придерживаются этого понятия. В отличие от функций, использующих обычные вызовы, эти развертывающие оконечные вызовы должны восстанавливать сохраненные регистры вызываемого абонента перед переходом к подпрограмме, а не когда они возвращаются сами. Обычно это не проблема, потому что на данный момент самой вызывающей функции верхнего уровня нет дела до значения, хранящегося в %ebx, но независимый от позиции код зависит от этого значения при каждой команде перехода, вызова или перехода.

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

Также setjmp и longjmp, конечно, проблематично, поскольку это фактически означает, что функция может завершить выполнение более одного раза, прежде чем она фактически завершится. Сложно или невозможно оптимизировать во время компиляции!

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

1 голос
/ 18 августа 2010

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

0 голосов
/ 20 августа 2018

Обычно компиляторы распознают ситуации, когда функции не нужно ничего делать после вызова другой, и заменяют этот вызов переходом. Многие случаи, когда это можно сделать безопасно, легко распознать, и такие случаи можно квалифицировать как «безопасный низко висящий фрукт». Однако даже для компиляторов, которые могут выполнять такую ​​оптимизацию, не всегда может быть очевидно, когда она должна или будет выполняться. Различные факторы могут сделать стоимость хвостового вызова большей, чем стоимость обычного вызова, и эти факторы не всегда могут быть предсказуемыми. Например, если функция заканчивается на return foo(1,2,3,a,b,c,4,5,6);, может оказаться целесообразным скопировать a, b и c в регистры, очистить стек и затем подготовить аргументы для передачи, но может быть недостаточно регистров, доступных для обработки foo(a,b,c,d,e,f,g,h,i); аналогично.

Если бы язык имел специальный синтаксис "хвостового вызова", который требовал, чтобы компиляторы давали задание, которое делает хвостовой вызов, если это вообще возможно, и отказывалось от компиляции в противном случае, код мог бы смело предполагать, что такие функции могут быть вложены сколь угодно глубоко. Однако при использовании обычного синтаксиса вызова нет общего способа узнать, сможет ли компилятор выполнить хвостовой вызов дешевле, чем «обычный».

0 голосов
/ 20 августа 2018

Для тех, кто любит доказательство по построению, вот Godbolt, выполняющий хорошую оптимизацию хвостового вызова и встроенный: https://godbolt.org/z/DMleUN

Однако, если вы проворачиваете оптимизацию до -O3 (или, без сомнения, ждетенесколько лет или использовать другой компилятор), оптимизация полностью удаляет цикл / рекурсию.

Вот пример, который оптимизирует до одной инструкции даже с -O2: https://godbolt.org/z/CNzWex

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...