Чому код активно намагається запобігти оптимізації зворотних викликів?


81

Заголовок запитання може бути дещо дивним, але справа в тому, що, наскільки мені відомо, взагалі немає нічого, що могло б говорити проти оптимізації виклику хвоста. Однак, переглядаючи проекти з відкритим кодом, я вже натрапив на кілька функцій, які активно намагаються перешкодити компілятору виконувати оптимізацію хвостового виклику, наприклад, реалізація 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
}

Я хотів би знати, чому це здається настільки важливим, і чи були випадки, коли б мені, як звичайному розробнику, теж слід було пам’ятати про це? Напр. чи є загальні підводні камені при оптимізації виклику хвоста?


10
Однією з можливих підводних каменів може бути те, що програма працює безперебійно на декількох платформах, а потім раптово перестає працювати при компіляції з компілятором, який не підтримує оптимізацію хвостових викликів. Пам'ятайте, що ця оптимізація насправді може не тільки підвищити продуктивність, але і запобігти помилкам виконання (переповнення стека).
Ніклас Б.

5
@NiklasB. Але хіба це не причина не намагатись його відключити?
JustSid

4
Системний дзвінок може бути надійним способом розподілу TCO, але також досить дорогим.
Fred Foo

39
Це чудовий навчальний момент для правильного коментування. +1 за часткове пояснення, чому існує цей рядок (для запобігання оптимізації зворотних викликів), -100 за не пояснення, чому оптимізацію зворотних викликів потрібно було відключити в першу чергу ...
Марк Совул,

16
Оскільки значення getpid()не використовується, чи не могло воно бути видалене інформованим оптимізатором (оскільки getpidце функція, яка, як відомо, не має побічних ефектів), отже, дозволяючи компілятору зробити оптимізацію хвостового виклику в будь-якому випадку? Це здається справді неміцним механізмом.
luiscubal

Відповіді:


83

Я припускаю, що це для того, щоб забезпечити наявність __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__у трасі стека для цілей налагодження. Це підтверджує __attribute__((no inline))цю ідею.

Якщо ви помітите, ця функція в будь-якому випадку просто переходить і переходить до іншої функції, тож це форма батута, про яку я можу тільки подумати, що існує з такою багатослівною назвою для полегшення налагодження. Це було б особливо корисно з огляду на те, що функція викликає покажчик функції, який був зареєстрований з іншого місця, і тому ця функція може не мати доступних символів налагодження.

Зверніть увагу також на інші функції з подібним іменем, які виконують подібні речі - справді схоже, що він допомагає побачити, що трапилось із зворотного сліду. Майте на увазі, що це основний код Mac OS X, і він також відображатиметься у звітах про збої та оброблятиме зразки звітів.


Так, це відповідає __attribute__((noinline)). Я думаю, ви тут на місці.
Ніклас Б.

Так, справді має сенс. Але якщо ви подивитеся, звідки ці функції викликаються, ви побачите, що вони завжди викликаються лише з однієї функції, наприклад, моя __CFRunLoopDoObservers
прикладна

1
Звичайно, але, мабуть, це ще один маркер того , де саме запускається зворотний виклик / блок / тощо спостерігача.
mattjgalloway

2
Я думаю, що це найкраща відповідь. +1
R .. GitHub СТОП ДОПОМОГАЙ ЛЕД

@R .. Хоча я можу прийняти лише одну відповідь, і Ендрю Уайт також назвав інші випадки, коли оптимізація хвостового виклику може не потрібна. Пам’ятайте, я не запитував, чому це зробила функція, а чому це взагалі не бажано, і дав функцію як приклад із реального світу.
JustSid

34

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

Оскільки розглянутий метод нічого не поміщає в стек, оптимізація рекурсії зворотного виклику може видавати код, який би входив у нескінченний цикл, на відміну від неоптимізованого коду, який міститиме адресу повернення в стеку який врешті-решт переллється у разі неправильного використання.

Єдина моя думка пов’язана із збереженням викликів у стеку для налагодження та друку стека.


8
Я думаю, що пояснення стека / налагодження є набагато більш імовірним (і я збирався його опублікувати). Нескінченний цикл насправді не гірший, ніж збій, оскільки користувач може змусити програму вийти. Це також пояснювало б ноінлайн.
ughoavgfhw

3
@ughoavgfhw: можливо, але коли ти потрапляєш у різьбу та подібні, нескінченні цикли дуже важко відстежити. Я завжди думав, що зловживання має спричинити виняток. Оскільки мені ніколи цього не доводилось робити, це все одно лише здогадки.
Ендрю Уайт

1
синхронність, щось на кшталт ... Я щойно натрапив на помилку, через яку програма не відкривала нові вікна. Це змушує задуматись, якби додаток зазнав аварії перед спробою наситити "купу" (мою пам'ять) і задушити X, мені не потрібно було б переходити на термінал, щоб різко вбити божевільний додаток (оскільки X почав незабаром ставати не реагує). То, можливо, це було б причиною віддати перевагу підходу "швидкого провалу", який міг би мати переповнення стека та відсутність оптимізації ...? а може, це просто інша справа, хоча ...!
ShinTakezou

2
@AndrewWhite Хм, я дуже люблю нескінченні цикли - я не можу придумати жодної речі, яку простіше налагоджувати, я маю на увазі, що ви можете просто прикріпити свій налагоджувач і отримати точне положення та стан проблеми, не здогадуючись. Але якщо ви хочете отримати стек-траси від користувачів, я погоджуюсь, що нескінченний цикл є проблематичним, тому це видається логічним - у вашому журналі з’явиться помилка, нескінченного циклу немає.
Voo

1
Це передбачає, що функція в першу чергу є рекурсивною, але це не так; ні безпосередньо, ні (дивлячись на контекст, звідки походить функція) опосередковано. Спочатку я зробив те саме помилкове припущення.
Конрад Рудольф

21

Однією з можливих причин є полегшення налагодження та профілювання (за допомогою TCO кадр батьківського стека зникає, що робить сліди стека важче зрозуміти).


2
полегшення профілювання за рахунок уповільнення програми є якось дивним. Це має такий самий сенс, як розбавляти масло перед тим, як виміряти, наскільки далеко може проїхати ваша машина: х
Матьє М.,

1
@MatthieuM .: Така річ не мала б сенсу, якби доданий виклик виконувався мільйони разів у циклі, але якщо він виконується кілька сотень разів на секунду або менше, можливо, краще залишити його в реальній системі і мати можливість вивчити, як поводиться реальна система, ніж вийняти її та ризикувати, якщо таке видалення внесе незначну, але важливу зміну в поведінку системи.
supercat

@MatthieuM. Якщо розведення масла є обов’язковою умовою для будь-якого вимірювання, то насправді це цілком логічно.
Дмитро Григор’єв

@DmitryGrigoryev: Ні, ні. Жодна міра не дратує, але неправильна міра від марної до небезпечної (залежно від того, наскільки ви довіряєте їй). Продовжуючи аналогію з маслом: якщо це уповільнить роботу, то ви можете отримати заходи, які вказують на те, що вага важливіша за аеродинаміку, і, таким чином, усунути вагу та погіршити аеродинаміку, щоб оптимізувати те, що ви виміряли ... однак із справжньою олією, коли ви їдете швидше, виявляється, що аеродинаміка була важливішою, а ваше "вдосконалення" гірше, ніж нічого не робити!
Матьє М.,

@MatthieuM. Чи знайомий ви з принципом невизначеності? Будь-яке вимірювання є певною мірою помилковим, оскільки немає можливості виміряти що-небудь без взаємодії з об’єктом, що вимірюється. Отже, навіть якщо ви не зміните масло у вашому прикладі, оснащення автомобіля все одно змінить аеродинаміку.
Дмитро Григор’єв
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.