Зачем коду активно пытаться предотвратить оптимизацию хвостового вызова?

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

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (func) {
        func(observer, activity, info);
    }
    getpid(); // thwart tail-call optimization
}

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


person JustSid    schedule 28.05.2012    source источник
comment
Одна из возможных ловушек может заключаться в том, что приложение плавно работает на нескольких платформах, а затем внезапно перестает работать при компиляции с компилятором, который не поддерживает оптимизацию хвостового вызова. Помните, что эта оптимизация может не только повысить производительность, но и предотвратить ошибки времени выполнения (переполнение стека).   -  person Niklas B.    schedule 29.05.2012
comment
@NiklasB. Но разве это не причина, чтобы не пытаться отключить его?   -  person JustSid    schedule 29.05.2012
comment
Системный вызов может быть надежным способом снижения совокупной стоимости владения, но также довольно дорогим.   -  person Fred Foo    schedule 29.05.2012
comment
@JustSid: Нет, если вас интересует переносимая программа, не зависящая от конкретных оптимизаций компилятора.   -  person Niklas B.    schedule 29.05.2012
comment
У вас есть и другие примеры? Может быть, разработчик этой конкретной библиотеки лично опасается оптимизации хвостового вызова?   -  person Shahbaz    schedule 29.05.2012
comment
Действительно, в GCC __asm__ __volatile__ ( "" : : "memory" ); было бы намного дешевле сделать это.   -  person R.. GitHub STOP HELPING ICE    schedule 29.05.2012
comment
@Shahbaz Я видел это и в других библиотеках / проектах, но я не помню, какие именно (хотя они использовали некоторые __asm__ volatile вместо системного вызова. Я отредактирую вопрос, когда найду их снова, но похоже, что это либо более распространенный страх или проблема, которую они пытаются избежать   -  person JustSid    schedule 29.05.2012
comment
__attribute__((noinline)) тоже выглядит подозрительно. Может быть, автор зависит от очень специфического поведения среды выполнения в отношении настройки стека?   -  person Niklas B.    schedule 29.05.2012
comment
Не уверен, что это вообще актуально, но я подумал, что может быть проблема, в частности, с оптимизацией хвостового вызова и os x, и я наткнулся на этот поиск в Google, который, по-видимому, говорит, что вызывает проблемы с трассировкой стека или чем-то еще.   -  person Shahbaz    schedule 29.05.2012
comment
Это отличный поучительный момент для правильного комментирования. +1 за частичное объяснение, почему эта строка существует (чтобы предотвратить оптимизацию хвостового вызова), -100 за то, что не объясняет, почему оптимизацию хвостового вызова нужно было вообще отключить ...   -  person Mark Sowul    schedule 29.05.2012
comment
Поскольку значение getpid() не используется, не может ли его удалить информированный оптимизатор (поскольку getpid - это функция, которая, как известно, не имеет побочных эффектов), что позволяет компилятору в любом случае выполнять оптимизацию хвостового вызова? Это кажется действительно хрупким механизмом.   -  person luiscubal    schedule 29.05.2012
comment
@ArjunShankar Ты прав, забудь. Ответы (особенно принятые) предполагали хвостовую рекурсию, это меня сбило с толку.   -  person Konrad Rudolph    schedule 29.05.2012
comment
@luiscubal, как компилятор узнает об отсутствии побочных эффектов? вызов библиотеки C getpid () может вызвать системный вызов ОС sys_getpid, который должен иметь побочные эффекты. в действительности, большинство реализаций библиотеки C изменяют внутренние переменные при вызове getpid (). В первый раз, когда я вызываю getpid (), я перехожу к ядру, и библиотека кеширует возвращаемое значение. в следующий раз, когда я вызываю getpid (), библиотека вернет кешированное значение.   -  person Chris    schedule 30.07.2013
comment
@Chris Ну, x = getpid(); x = getpid(); то же самое, что x = getpid();, поэтому его действительно можно оптимизировать. Тот факт, что результат кэшируется и первый раз попадает в ОС, не имеет значения, поскольку нет count_number_of_times_getpid_was_called функции. Оптимизатору не обязательно предполагать, что вызов имеет побочные эффекты. Если результат всегда один и тот же, он может быть в каком-то белом списке функций, который, как известно, безопасен и поэтому может быть оптимизирован.   -  person luiscubal    schedule 30.07.2013
comment
@luiscubal Как правило, такая функция, как getpid, связана динамически, поэтому компиляторы не имеют представления о том, что будет делать getpid. Даже если код getpid был доступен компилятору, как я описывал ранее, getpid действительно сказал эффекты и вызывает системный вызов, тем самым вызывая вызов. Максимум x = getpid(); x = getpid(); можно оптимизировать до getpid(); x=getpid();   -  person Chris    schedule 30.07.2013
comment
@Chris Если getpid был объявлен с __attribute__ ((pure)), они могли бы. Он не оптимизирует его, поэтому я предполагаю, что у него нет этого атрибута. Обратите внимание, что если бы у него был атрибут в заголовке, тип связи не имел бы значения. А что им мешает добавить в шапку? Если стандарт гарантирует, что getpid всегда возвращает одно и то же значение, тогда ответ - ничего - и если вы измените реализацию, это ваша проблема.   -  person luiscubal    schedule 30.07.2013
comment
Я бы хотел, чтобы был атрибут или что-то, что сообщало бы компилятору, чтобы он не оптимизировал конкретную функцию с помощью хвостового вызова, без необходимости заставлять программу делать бесполезные вызовы во время выполнения.   -  person user102008    schedule 29.04.2015


Ответы (3)


Я предполагаю, что это необходимо, чтобы убедиться, что __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ находится в трассировке стека для целей отладки. В нем есть __attribute__((no inline)), которые подтверждают эту идею.

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

Обратите также внимание на другие функции с похожими именами, которые делают похожие вещи - действительно похоже, что они существуют, чтобы помочь увидеть, что произошло, из обратной трассировки. Имейте в виду, что это основной код Mac OS X, который будет отображаться в отчетах о сбоях, а также обрабатывать образцы отчетов.

person mattjgalloway    schedule 28.05.2012
comment
Да, это согласуется с __attribute__((noinline)). Я думаю, ты здесь на месте. - person Niklas B.; 29.05.2012
comment
Да, действительно имеет смысл. Но если вы посмотрите, откуда эти функции вызываются, вы увидите, что они всегда вызываются только из одной функции, например, моя примерная функция вызывается только из __CFRunLoopDoObservers, что определенно отображается в трассировке стека ... - person JustSid; 29.05.2012
comment
Конечно, но я предполагаю, что это еще один маркер для именно, где выполняется обратный вызов / блок / и т.д. наблюдателя. - person mattjgalloway; 29.05.2012
comment
Думаю, это лучший ответ. +1 - person R.. GitHub STOP HELPING ICE; 29.05.2012
comment
@R .. Я могу принять только один ответ, и Эндрю Уайт также назвал другие случаи, когда оптимизация хвостового вызова могла быть нежелательной. Помните, я не спрашивал, почему функция это делает, а почему это может быть нежелательно в целом, и привел функцию в качестве реального примера. - person JustSid; 29.05.2012

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

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

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

person Andrew White    schedule 28.05.2012
comment
Я думаю, что объяснение трассировки стека / отладки гораздо более вероятно (и я собирался опубликовать его). Бесконечный цикл на самом деле не хуже, чем сбой, поскольку пользователь может заставить приложение завершить работу. Это также объяснило бы noinline. - person ughoavgfhw; 29.05.2012
comment
@ughoavgfhw: возможно, но когда вы попадаете в многопоточность и тому подобное, бесконечные циклы действительно трудно отследить. Я всегда считал, что неправильное использование должно вызывать исключение. Поскольку мне никогда не приходилось этого делать, это всего лишь предположение. - person Andrew White; 29.05.2012
comment
синхронность, вроде ... Я только что столкнулся с серьезной ошибкой, из-за которой приложение не открывало новые окна. Это заставляет меня думать, что если бы приложение аварийно завершилось, прежде чем пытаться заполнить кучу (мою память) и задушить X, мне не нужно было бы переключаться на терминал, чтобы внезапно убить сумасшедшее приложение (поскольку X начал вскоре перестать отвечать) . Так что, может быть, это будет поводом предпочесть отказоустойчивый подход, который может сопровождаться переполнением стека и отсутствием оптимизации ...? а может это совсем другое дело ...! - person ShinTakezou; 29.05.2012
comment
@AndrewWhite Хм, мне очень нравятся бесконечные циклы - я не могу придумать ни одной вещи, которую было бы легче отлаживать, я имею в виду, что вы можете просто прикрепить свой отладчик и получить точное положение и состояние проблемы, не догадываясь. Но если вы хотите получить трассировку стека от пользователей, я согласен с тем, что бесконечный цикл проблематичен, поэтому это кажется логичным - в вашем журнале появится ошибка, а бесконечный цикл - нет. - person Voo; 29.05.2012
comment
Сохранение стека вызовов - единственная причина, по которой я использовал это на практике. С TCO, если int A () вызывает B () вызывает C (), а B- ›C () является хвостовым вызовом, тогда, если что-то выйдет из строя внутри C (), стек вызовов будет выглядеть как A, вызванный C напрямую, без перехода через B. Это может сбивать с толку. - person Crashworks; 29.05.2012
comment
Это предполагает, что функция в первую очередь рекурсивна, но это не так; ни прямо, ни (глядя на контекст, из которого происходит функция) косвенно. Сначала я сделал то же ошибочное предположение. - person Konrad Rudolph; 29.05.2012

Одна из возможных причин - упростить отладку и профилирование (при TCO исчезает кадр родительского стека, что затрудняет понимание трассировки стека).

person NPE    schedule 28.05.2012
comment
Однако упрощение профилирования за счет замедления работы программы - это довольно странно. Это имеет такой же смысл, как разбавление масла перед измерением того, как далеко может проехать ваша машина: x - person Matthieu M.; 29.05.2012
comment
@MatthieuM .: Такая вещь не имела бы смысла, если бы добавленный вызов выполнялся миллионы раз в цикле, но если он выполняется несколько сотен раз в секунду или меньше, может быть лучше оставить его в реальной системе и уметь исследовать, как ведет себя реальная система, чем снимать ее и рисковать тем, что такое удаление приведет к тонкому, но важному изменению в поведении системы. - person supercat; 24.02.2015
comment
@MatthieuM. Если разбавление масла является предпосылкой для любого измерения, тогда это действительно имеет смысл. - person Dmitry Grigoryev; 25.11.2016
comment
@DmitryGrigoryev: Нет. Никакая мера не раздражает, но неправильная мера превратится из бесполезной в опасную (в зависимости от того, насколько вы ей доверяете). Продолжая аналогию с маслом: если оно замедляет вас, вы можете получить показатели, указывающие на то, что вес важнее аэродинамики, и, таким образом, уменьшить вес и ухудшить аэродинамику, чтобы оптимизировать то, что вы измерили ... однако с настоящим маслом, когда едешь быстрее, оказывается, что аэродинамика важнее, а твои улучшения хуже, чем ничего не делать! - person Matthieu M.; 25.11.2016
comment
@MatthieuM. Вы знакомы с принципом неопределенности? Любое измерение в какой-то степени неверно, потому что невозможно измерить что-либо, не взаимодействуя с измеряемым объектом. Таким образом, даже если вы не меняете масло в своем примере, приборное оснащение автомобиля все равно изменит аэродинамику. - person Dmitry Grigoryev; 25.11.2016
comment
@DmitryGrigoryev: Некоторую неопределенность, конечно, нельзя избежать, но есть континуум между небольшими возмущениями, которые снижают точность, и большими возмущениями, которые делают измерение совершенно несущественным. Предотвращение TCO оказывает большое влияние на глубоко рекурсивные вызовы функций: накладные расходы на вызов, потребление стека подразумевают загрязнение кеша L1 и т. Д. И, конечно, не позволяют оптимизатору превращать рекурсивные вызовы в цикл в худшем случае (особенно если он предотвращает компилятор от оценки вычисления во время компиляции). - person Matthieu M.; 25.11.2016
comment
@MatthieuM. Да, но совокупная стоимость владения не ограничивается рекурсивным кодом, любая функция, которая заканчивается вызовом, может быть оптимизирована таким образом. Конечно, профилирование рекурсивных вещей с отключенной TCO не имеет смысла (но тогда я могу сказать без какого-либо профилирования, в чем проблема;), но вопрос не в рекурсивных функциях в первую очередь. - person Dmitry Grigoryev; 25.11.2016