Це клоп Кланг
... при вкладенні функції, що містить нескінченну петлю. Поведінка відрізняється, коли while(1);
з'являється безпосередньо в основному, що пахне мені дуже глючно.
Дивіться відповідь @ Arnavion для підсумків та посилань. Решта цієї відповіді була написана ще до того, як я отримав підтвердження, що це помилка, не кажучи вже про відому помилку.
Щоб відповісти на заголовкове запитання: Як зробити нескінченний порожній цикл, який не буде оптимізований? ? -
зробіть die()
макрос, а не функцію , щоб обійти цю помилку в Clang 3.9 та пізніших версіях. (Більш ранні версії Clang або зберігають цикл, або випромінюютьcall
не-вбудовану версію функції з нескінченним циклом.) Це, здається, є безпечним, навіть якщо print;while(1);print;
функція вбудовується до його виклику ( Godbolt ). -std=gnu11
vs. -std=gnu99
нічого не змінює.
Якщо ви дбаєте лише про GNU C, P__J ____asm__("");
всередині циклу також працює, і це не повинно зашкодити оптимізації будь-якого оточуючого коду для будь-яких компіляторів, які його розуміють. Висловлювання GNU C Basic asm неявноvolatile
, тому це вважається видимим побічним ефектом, який повинен "виконуватись" стільки разів, скільки було б у абстрактній машині C. (І так, Кланг реалізує GNU-діалект C, як це підтверджено в посібнику GCC.)
Деякі люди стверджують, що може бути законним оптимізувати порожній нескінченний цикл. Я не погоджуюся 1 , але навіть якщо ми це погоджуємося, це також не може бути законним, щоб Clang припускав твердження після того, як цикл недоступний, і нехай виконання впаде з кінця функції в наступну функцію або в сміття що розшифровується як випадкові вказівки.
(Це було б сумісно зі стандартами для Clang ++ (але все ще не дуже корисно); нескінченні цикли без будь-яких побічних ефектів є UB в C ++, але не C.
Це поки (1); невизначена поведінка в C? UB дозволяє компілятору випускати в основному все, що завгодно для коду на шляху виконання, який неодмінно зіткнеться з UB. Оператор asm
у циклі уникає цього UB для C ++. Але на практиці компіляція Clang як C ++ не видаляє нескінченні порожні петлі постійного вираження, за винятком випадків, коли вбудовані, як і коли складання як C.)
Вручну вкладиш while(1);
змінює те, як Кланг компілює його: нескінченний цикл, присутній в ASM Це те, чого ми очікували від юриста з питань правил.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
У досліднику компілятора Godbolt компілюється Clang 9.0 -O3 як C ( -xc
) для x86-64:
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
Той самий компілятор з тими ж параметрами компілює a, main
який викликає infloop() { while(1); }
той самий спершу puts
, але потім просто припиняє видавати інструкції для main
після цього пункту. Отже, як я вже сказав, виконання просто відпадає від кінця функції, у будь-яку функцію, яка є наступною (але зі стеком, не зміненим для введення функції, так що це навіть не дійсний зворотний виклик).
Дійсні варіанти були б
- випромінюють
label: jmp label
нескінченну петлю
- або (якщо ми визнаємо, що нескінченний цикл можна зняти), надішліть інший виклик для друку 2-ої рядок, а потім
return 0
з main
.
Збій або продовження іншим способом без друку "недосяжних" явно не нормально для реалізації C11, якщо тільки немає UB, якого я не помітив.
Зноска 1:
Для запису я погоджуюся з відповіддю @ Лундіна, яка цитує стандарт доказів того, що C11 не допускає припущення припинення для нескінченних циклів постійної експресії, навіть коли вони порожні (відсутність вводу / виводу, мінливість, синхронізація чи інше видимі побічні ефекти).
Це набір умов, за допомогою яких цикл може скластись у порожній цикл asm для звичайного процесора. (Навіть якщо тіло не було порожнім у джерелі, призначення змінних не може бути видимим іншим потокам чи обробникам сигналів без UB-перегону даних, поки цикл працює. Отже, відповідна реалізація може видалити такі тіла циклу, якщо вона хотіла Тоді це залишає питання про те, чи можна зняти саму петлю. ISO C11 прямо говорить, що ні)
Зважаючи на те, що C11 виділяє цей випадок як той, де реалізація не може припустити, що цикл закінчується (і що це не UB), здається, що вони мають намір цикл бути присутнім під час виконання. Реалізація, орієнтована на процесори з моделлю виконання, яка не може виконувати нескінченний обсяг роботи за обмежений час, не має обґрунтування для видалення порожнього постійного нескінченного циклу. Або навіть загалом, точне формулювання стосується того, чи можна їх «вважати припиненими» чи ні. Якщо цикл не може завершитися, це означає, що пізніше код не буде доступний, незалежно від того того, які аргументи ви робите щодо математики та нескінченності та скільки часу потрібно, щоб виконати нескінченну кількість роботи на якійсь гіпотетичній машині.
Крім того, Clang не просто сумісний з ISO C DeathStation 9000, він призначений бути корисним для програмування реальних систем низького рівня, включаючи ядра та вбудовані речі. Тож чи приймаєте ви чи не аргументи щодо C11, що дозволяють видалити while(1);
, не має сенсу, що Кланг хотів би насправді це зробити. Якщо ти пишешwhile(1);
, це, ймовірно, не було випадковістю. Видалення циклів, які закінчуються нескінченно випадково (з виразами керування змінними під час виконання), може бути корисним, і компілятори мають сенс робити це.
Рідко ви хочете просто обертатися до наступного переривання, але якщо ви пишете це на C, це точно те, що ви очікуєте, що це станеться. (І що робить відбувається в GCC і Clang, за винятком випадків , коли Clang нескінченний цикл всередині функції - оболонки).
Наприклад, у примітивному ядрі ОС, коли планувальник не має завдань для його запуску, він може виконати завдання в режимі очікування. Перша реалізація цього може бути while(1);
.
Або для обладнання без будь-якої функції енергозбереження в режимі очікування, яка може бути єдиною реалізацією. (До початку 2000-х, я вважаю, що це не рідкість на x86. Хоча hlt
інструкція існувала, IDK, якщо вона економила значну кількість енергії, поки процесори не почали працювати з режимами очікування з низькою потужністю.)