Як зробити нескінченний порожній цикл, який не буде оптимізований?


131

Здається, що стандарт C11 означає, що операції ітерації з постійними керуючими виразами не повинні оптимізуватися. Я приймаю поради з цієї відповіді , яка конкретно цитує розділ 6.8.5 проекту проекту:

Ітераційний оператор, керуючий вираз якого не є постійним виразом ..., може припускати, що реалізація припиняється.

У цій відповіді зазначається, що цикл на зразок while(1) ;не повинен піддаватися оптимізації.

Отже ... чому Clang / LLVM оптимізує цикл нижче (компілюється з cc -O2 -std=c11 test.c -o test)?

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

На моїй машині це роздруковується begin, а потім виходить з ладу за незаконною інструкцією ( ud2пастка, розміщена після die()). На godbolt ми бачимо, що нічого не створюється після виклику до puts.

Напрочуд складною задачею було змусити Кланг вивести нескінченний цикл під -O2- хоча я міг неодноразово перевіряти volatileзмінну, яка передбачає прочитану пам'ять, яку я не хочу. І якщо я роблю щось подібне:

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    volatile int x = 1;
    if(x)
        die();
    printf("unreachable\n");
}

... Відбитки Кланг, beginза якими unreachableніби нескінченна петля ніколи не існувала.

Як змусити Clang вивести правильний нескінченний цикл доступу без пам’яті з увімкненими оптимізаціями?


3
Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Bhargav Rao

2
Не існує портативного рішення, яке не передбачає побічного ефекту. Якщо ви не хочете отримати доступ до пам'яті, найкращою надією буде зареєструвати непостійний неподписаний знак; але реєстр відходить у C ++ 17.
Скотт М

25
Можливо, це не входить до сфери питання, але мені цікаво, чому ти хочеш це зробити. Звичайно, є якийсь інший спосіб виконати своє справжнє завдання. Або це просто академічний характер?
Cruncher

1
@Cruncher: Ефекти будь-якої конкретної спроби запуску програми можуть бути корисними, по суті марними або істотно гіршими, ніж марними. Виконання, в результаті якого програма застряє в нескінченному циклі, може виявитися марною, але все-таки кращою для іншої поведінки, яку компілятор може замінити.
supercat

6
@Cruncher: Тому що код може працювати у вільно розташованому контексті, коли його немає exit(), і тому, що код може виявити ситуацію, коли він не може гарантувати, що наслідки подальшого виконання не будуть гіршими, ніж марні . Цикл «Перехід до самої» - це досить вразливий спосіб вирішення подібних ситуацій, але, тим не менш, це може бути найкращим способом вирішення поганої ситуації.
supercat

Відповіді:


77

Стандарт C11 говорить про це, 6.8.5 / 6:

Ітераційний оператор, керуючий вираз якого не є постійним виразом, 156), який не виконує операцій введення / виводу, не здійснює доступу до летючих об'єктів і не виконує синхронізації чи атомних операцій у своєму тілі, керуючи виразом, або (у випадку заява) його вираз-3, може припускати, що реалізація припиняється. 157)

Дві ноги ноги не є нормативними, але дають корисну інформацію:

156) Опущений керуючий вираз замінюється ненульовою постійною, яка є постійним виразом.

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

У вашому випадку while(1)є кристально чистим постійним виразом, тому реалізація може не припускати його припинення. Така реалізація була б безнадійно порушена, оскільки циклічні сповіщення - це звичайна побудова програмування.

Що ж відбувається з "недосяжним кодом" після циклу, однак, наскільки я знаю, недостатньо чітко визначено. Однак Кланг дійсно поводиться дуже дивно. Порівняння машинного коду з gcc (x86):

gcc 9.2 -O3 -std=c11 -pedantic-errors

.LC0:
        .string "begin"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
.L2:
        jmp     .L2

кланг 9.0.0 -O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.Lstr:
        .asciz  "begin"

gcc генерує цикл, кланг просто біжить у ліс і виходить з помилкою 255.

Я схиляюся до того, що це невідповідна поведінка клангу. Тому що я намагався розширити ваш приклад далі так:

#include <stdio.h>
#include <setjmp.h>

static _Noreturn void die() {
    while(1)
        ;
}

int main(void) {
    jmp_buf buf;
    _Bool first = !setjmp(buf);

    printf("begin\n");
    if(first)
    {
      die();
      longjmp(buf, 1);
    }
    printf("unreachable\n");
}

Я додав C11 _Noreturn, намагаючись допомогти компілятору далі. Повинно бути зрозуміло, що ця функція зависне, лише з цього ключового слова.

setjmpповерне 0 при першому виконанні, тож ця програма повинна просто забитися до while(1)та зупинитися там, тільки друк "почнеться" (припускаючи, \ n змиває stdout). Це відбувається з gcc.

Якщо цикл було просто знято, він повинен надрукувати "почати" 2 рази, а потім надрукувати "недоступним". На clang , однак, ( Godbolt ), він надрукує " start " 1 раз, а потім "недоступний" перед поверненням коду виходу 0. Це просто неправильно, як би ви не ставили його.

Я не можу знайти жодного випадку, щоб заявляти про невизначену поведінку, тому я вважаю, що це помилка в лясканні. У будь-якому випадку така поведінка робить кланг на 100% марним для таких програм, як вбудовані системи, де ви просто повинні мати можливість покладатися на вічні петлі, що висять програму (під час очікування сторожової собаки тощо).


15
Я не погоджуюсь з "цим кришталево чистим постійним виразом, тому реалізація може не припускати його припинення" . Це дійсно потрапляє у виборювальну мову, але 6.8.5/6це у формі if (цих), то ви можете припустити (це) . Це не означає, якщо ні (ці) ви можете не припускати (це) . Це специфікація лише тоді, коли умови виконуються, а не тоді, коли вони не виконані, де ви можете робити все, що завгодно, дотримуючись стандартів. А якщо спостережень немає ...
kabanus

7
@kabanus Цитована частина - особливий випадок. Якщо ні (особливий випадок), оцініть і послідовно виконайте послідовність коду, як зазвичай. Якщо ви продовжуєте читати ту саму главу, керуючий вираз оцінюється так, як зазначено для кожного оператора ітерації ("як зазначено семантикою"), за винятком цитованого спеціального випадку. Він дотримується тих же правил, що й оцінка будь-якого обчислення значень, який є послідовно визначеним та визначеним.
Лундін

2
Я згоден, але ви б не surpised , що int z=3; int y=2; int x=1; printf("%d %d\n", x, z);немає 2в збірці, так і в порожній даремний сенс xне призначено після того, yале після того, як zз - за оптимізації. Тож, виходячи з вашого останнього речення, ми дотримуємось звичайних правил, припускаємо, що час зупинився (оскільки нас не обмежувало нічого кращого) і залишаємо остаточний, "недосяжний" друк. Тепер ми оптимізуємо цю марну заяву (бо краще не знаємо).
кабанус

2
@MSalters Один із моїх коментарів було видалено, але дякую за вклад - і я згоден. Що я сказав у коментарі, я думаю, що це суть дебатів - це while(1);те саме, що int y = 2;твердження з точки зору того, яку семантику нам дозволено оптимізувати, навіть якщо їх логіка залишається в джерелі. З n1528 у мене було враження, що вони можуть бути однаковими, але оскільки люди, досвідченіші за мене, міркують іншим способом, і, мабуть, це офіційна помилка, то поза філософськими дискусіями щодо того, чи є чітка формуляція в стандарті , аргумент подається спірним.
кабанус

2
"Така реалізація була б безнадійно порушена, оскільки циклічні сповіщення" навіки "- це звичайна побудова програмування". - Я розумію настрої, але аргумент є помилковим, оскільки він може бути застосований ідентично до C ++, але компілятор C ++, який оптимізував цей цикл, не буде порушеним, а відповідним.
Конрад Рудольф

52

Потрібно вставити вираз, який може викликати побічний ефект.

Найпростіше рішення:

static void die() {
    while(1)
       __asm("");
}

Годболт посилання


21
Однак це не пояснює, чому кланг виступає.
Лундін

4
Хоча сказати, що "це помилка в лясках" є досить. Я хотів би спробувати спробувати кілька речей тут, перш ніж кричати "помилка".
Лундін

3
@Lundin Я не знаю, чи це помилка. Стандарт технічно не є точним у цьому випадку
P__J__

4
На щастя, GCC є відкритим кодом, і я можу написати компілятор, який оптимізує ваш приклад. І я міг би зробити це для будь-якого прикладу, який ви придумали, зараз і в майбутньому.
Томас Веллер

3
@ThomasWeller: розробники GCC не приймуть виправлення, яке оптимізує цю петлю; це порушило б задокументовану = гарантовану поведінку. Дивіться мій попередній коментар: asm("")неявно, asm volatile("");і тому заява ASM повинна працювати стільки разів, скільки це робиться в абстрактній машині gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html . (Зверніть увагу , що це НЕ безпечно для його побічні ефекти включають в себе будь-яку пам'ять або регістри, вам потрібно Extended асемблер з "memory"CLOBBER , якщо ви хочете прочитати або запис в пам'ять , що ви коли - небудь доступ з C. Basic асемблері безпечний тільки такі речі , як asm("mfence")або cli.)
Пітер Кордес

50

Інші відповіді вже висвітлювали способи змусити Кланг випромінювати нескінченну петлю з вбудованою мовою складання або іншими побічними ефектами. Я просто хочу підтвердити, що це справді помилка компілятора. Зокрема, це давня помилка LLVM - вона застосовує концепцію C ++ про "всі петлі без побічних ефектів повинні припинятися" до мов, де не слід, наприклад, C.

Наприклад, мова програмування Rust також дозволяє нескінченно циклів і використовує LLVM як бекенд, і це має цю ж проблему.

За короткий термін виявляється, що LLVM продовжить вважати, що "всі петлі без побічних ефектів повинні припинитися". Для будь-якої мови, яка дозволяє нескінченно циклів, LLVM очікує, що передній кінець вставить llvm.sideeffectопдоди в такі петлі. Це те, що планує зробити Руст, тому Кланг (при компілюванні коду С), мабуть, повинен буде це зробити і.


5
Нічого подібного до запаху помилки, старшого десятиліття ... з кількома запропонованими виправленнями та виправленнями ... все ще не було виправлено.
Ян Кемп

4
@IanKemp: Для того, щоб виправити помилку, зараз знадобиться визнати, що для виправлення помилки знадобилося десять років. Краще сподіватися на те, що Стандарт зміниться, щоб виправдати свою поведінку. Звичайно, навіть якщо стандарт змінився, це все-таки не виправдовуватиме їх поведінку, окрім очей людей, які розглядали б зміни до Стандарту як ознаку того, що попередній мандат поведінки стандарту був дефектом, який слід виправити заднім числом.
supercat

4
Це було "зафіксовано" в тому сенсі, що LLVM додав sideeffectоп (у 2017 році) і очікує, що передні кінці вставлять цю опцію в петлі на власний розсуд. LLVM повинен був вибрати деякий за замовчуванням цикл, і трапилося вибрати той, який узгоджується з поведінкою C ++, навмисно чи іншим чином. Звичайно, ще належить зробити деяку оптимізаційну роботу, наприклад, об'єднати послідовні sideeffectопераційні операції в один. (Це те, що перешкоджає його використанню передньої частини Rust.) Отже, виходячи з цього повідомлення, помилка знаходиться в передній частині (clang), яка не вставляє оп в циклі.
Арнавіон

@Arnavion: Чи є якийсь спосіб вказати, що операції можуть бути відкладені, якщо або до тих пір, поки результати не будуть використані, але якщо дані не призведуть до безперервного циклу роботи програми, спроба продовжити залежність від даних зробить програму гіршою, ніж марною ? Необхідність додавати підроблені побічні ефекти, які заважають колишнім корисним оптимізаціям, щоб запобігти оптимізатору зробити програму гіршою за непотрібну, не звучить як рецепт ефективності.
supercat

Ця дискусія, ймовірно, належить до списків розсилки LLVM / clang. FWIW зобов'язання LLVM, яке додало оп, також навчило кілька проходів оптимізації щодо цього. Також Руст експериментував із вставкою sideeffectops до початку кожної функції і не бачив жодної регресії продуктивності. Єдине питання - регресія часу компіляції , очевидно, через відсутність злиття послідовних операцій, як я згадував у своєму попередньому коментарі.
Арнавіон

32

Це клоп Кланг

... при вкладенні функції, що містить нескінченну петлю. Поведінка відрізняється, коли while(1);з'являється безпосередньо в основному, що пахне мені дуже глючно.

Дивіться відповідь @ Arnavion для підсумків та посилань. Решта цієї відповіді була написана ще до того, як я отримав підтвердження, що це помилка, не кажучи вже про відому помилку.


Щоб відповісти на заголовкове запитання: Як зробити нескінченний порожній цикл, який не буде оптимізований? ? -
зробіть die()макрос, а не функцію , щоб обійти цю помилку в Clang 3.9 та пізніших версіях. (Більш ранні версії Clang або зберігають цикл, або випромінюютьcall не-вбудовану версію функції з нескінченним циклом.) Це, здається, є безпечним, навіть якщо print;while(1);print;функція вбудовується до його виклику ( Godbolt ). -std=gnu11vs. -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, якщо вона економила значну кількість енергії, поки процесори не почали працювати з режимами очікування з низькою потужністю.)


1
З цікавості, хто насправді використовує кланг для вбудованих систем? Я ніколи цього не бачив і працюю виключно із вбудованими. gcc тільки "нещодавно" (10 років тому) вийшов на вбудований ринок, і я використовую це скептично, бажано з низькими оптимізаціями і завжди з -ffreestanding -fno-strict-aliasing. Він добре працює з ARM і, можливо, зі застарілим AVR.
Лундін

1
@Lundin: IDK про вбудований, але так, люди створюють ядра з клангом, принаймні іноді Linux. Імовірно, також Дарвін для MacOS.
Пітер Кордес

2
bugs.llvm.org/show_bug.cgi?id=965 ця помилка виглядає актуальною, але я не впевнений, що це ми бачимо тут.
bracco23

1
@lundin - Я впевнений, що ми використовували GCC (та багато інших наборів інструментів) для вбудованої роботи впродовж 90-х, з RTOS, як VxWorks та PSOS. Я не розумію, чому ви кажете, що GCC лише нещодавно вийшов на вбудований ринок.
Джефф Ліверман

1
@JeffLearman Став останнім часом мейнстрімом? У будь-якому разі, фіскальне жорстке псевдонім gcc трапилося лише після введення C99, і новіші версії його вже не здаються бананами, коли вони стикаються з суворими порушеннями. Тим не менш, я залишаюся скептичним, коли використовую. Що стосується кланг, то остання версія, очевидно, повністю зламана, коли мова йде про вічні петлі, тому її не можна використовувати для вбудованих систем.
Лундін

14

Тільки для запису, Кланг також погано поводиться з goto:

static void die() {
nasty:
    goto nasty;
}

int main() {
    int x; printf("begin\n");
    die();
    printf("unreachable\n");
}

Це дає такий самий вихід, як у питанні, тобто:

main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

Я не бачу жодного способу прочитати це як дозволено в C11, де сказано лише:

6.8.6.1 (2) gotoЗаява викликає безумовний перехід до оператора, префіксованого названою міткою у функції, що додає.

Оскільки gotoце не "заява про ітерацію" (6.8.5 списки while, doтаfor ) нічого про спеціальні індульгенції, що передбачаються припинення, не застосовується, однак ви хочете їх прочитати.

За оригінальним компілятором посилання Godbolt посилання на x86-64 Clang 9.0.0, а прапорці є -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c

З іншими, такими як x86-64 GCC 9.2, ви отримуєте дуже досконалий:

.LC0:
  .string "begin"
main:
  sub rsp, 8
  mov edi, OFFSET FLAT:.LC0
  call puts
.L2:
  jmp .L2

Прапори: -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c


Відповідна реалізація може мати недокументований ліміт перекладу на час виконання або циклів процесора, що може викликати довільну поведінку при перевищенні, або якщо введення програм, що перевищує ліміт, неминуче. Такі питання є питаннями якості впровадження, поза межами юрисдикції стандарту. Здавалося б, дивним є те, що технічні працівники clang настільки наполягають на своєму праві виробляти неякісну реалізацію, але Стандарт це дозволяє.
supercat

2
@supercat спасибі за коментар ... чому б перевищення ліміту перекладу нічого іншого, крім провалу фази перекладу та відмови від виконання? Також: " 5.1.1.3 Діагностика Відповідна реалізація повинна видавати ... діагностичне повідомлення ... якщо блок попереднього перекладу або блок перекладу містить порушення будь-якого синтаксичного правила або обмеження ...". Я не бачу, як помилкова поведінка на етапі виконання може колись відповідати.
jonathanjo

Стандарт було б абсолютно неможливо застосувати, якби всі строки реалізації мали бути вирішені під час збирання, оскільки можна було написати програму строго узгодження, яка вимагала б більше байтів стека, ніж є атоми у Всесвіті. Незрозуміло, чи слід обмежувати час виконання "межею перекладу", але така концесія, очевидно, необхідна, і немає іншої категорії, в яку вона могла б бути встановлена.
supercat

1
Я відповідав на ваш коментар про "обмеження перекладу". Звичайно, є і обмеження на виконання, я, правда, не розумію, чому ви пропонуєте, щоб вони були обмежені обмеженнями для перекладу або чому ви вважаєте, що це необхідно. Я просто не бачу причин для того, щоб сказати, що це nasty: goto nastyможе відповідати, а не розкручувати процесор (і), поки користувач чи ресурси не втручаються.
jonathanjo

1
Стандарт не посилається на "обмеження виконання", які я міг знайти. Такі речі, як функція гніздування викликів функцій, зазвичай обробляються розподілом стеку, але відповідна реалізація, яка обмежує виклики функцій на глибину 16, може створювати 16 копій кожної функції, а виклик, який знаходиться в bar()межах, foo()обробляється як дзвінок з __1fooдо __2bar, з __2fooдо __3bar, і т.д. , і від __16fooдо __launch_nasal_demons, що дозволить потім все автоматичні об'єкти будуть статично, і буде робити те , що зазвичай «запускати часу» межа в межу перекладу.
supercat

5

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

Ітераційний оператор, керуючий вираз якого не є постійним виразом, 156), який не виконує операцій введення / виводу, не здійснює доступу до летючих об'єктів і не здійснює синхронізації чи атомних операцій у своєму тілі, керуючи виразом, або (у випадку для твердження) його вираз-3, може припускати, що реалізація припиняється.157)

Давайте розберемо це. Ітераційне твердження, яке задовольняє певним критеріям, може вважатись припиненим:

if (satisfiesCriteriaForTerminatingEh(a_loop)) 
    if (whatever_reason_or_just_because_you_feel_like_it)
         assumeTerminates(a_loop);

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

do { } while(0) або while(0){} є ітераційними висловлюваннями (циклами), які не відповідають критеріям, що дозволяють компілятору просто припускати примху, що вони припиняються, але вони, очевидно, припиняються.

Але чи може компілятор просто оптимізувати while(1){}?

5.1.2.3p4 говорить:

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

Це згадує вирази, а не висловлювання, тому це не на 100% переконливо, але, безумовно, дозволяє дзвінки типу:

void loop(void){ loop(); }

int main()
{
    loop();
}

пропустити. Цікаво, що Кланг пропускає його, а gcc - ні .


"Це нічого не говорить про те, що станеться, якщо критерії не задовольняються". Але це відбувається, 6.8.5.1. Висловлювання while: "Оцінка керуючого виразу відбувається перед кожним виконанням тіла циклу". Це воно. Це обчислення значень (постійного виразу), воно підпадає під правило абстрактної машини 5.1.2.3, що визначає термін оцінка: " Оцінка виразу в цілому включає як обчислення значень, так і ініціювання побічних ефектів". Відповідно до цієї ж глави, всі такі оцінки відстежуються і оцінюються відповідно до семантики.
Лундін

1
@Lundin Отже while(1){}, нескінченна послідовність 1оцінок переплітається з {}оцінкою, але де в стандарті йдеться, що ці оцінки потребують ненульового часу? Поведінка gcc є більш корисною, я думаю, тому що вам не потрібні хитрощі, пов’язані з доступом до пам'яті або хитрощами поза мовою. Але я не переконаний, що стандарт забороняє цю оптимізацію в лясках. Якщо здійснення while(1){}наміру не піддається оптимізації, стандарт повинен бути явним щодо цього, і нескінченне циклічне цикління повинно бути зазначено як помітний побічний ефект у 5.1.2.3p2.
PSkocik

1
Я думаю, що це визначено, якщо ви ставитеся до 1умови як до обчислення значень. Час виконання не має значення - важливо те, що while(A){} B;може бути не оптимізовано повністю, не оптимізовано B;та не повторно послідовно B; while(A){}. Якщо цитувати абстрактну машину C11, моє наголос: "Наявність точки послідовності між оцінкою виразів A і B означає, що кожне обчислення значення та побічний ефект, пов'язаний з A, секвенуються перед кожним обчисленням значення та побічним ефектом, пов'язаним з B ". Значення Aчітко використовується (за циклом).
Лундін

2
+1 Хоча мені здається, що "виконання висить на невизначений час без будь-якого результату" є "побічним ефектом" у будь-якому визначенні "побічний ефект", який має сенс і є корисним понад звичайний у вакуумі, це допомагає пояснити мислення, з якого це може мати сенс для когось.
mtraceur

1
Поруч "оптимізація нескінченного циклу" : Не зовсім зрозуміло, чи "це" відноситься до стандарту чи до компілятора - можливо, перефразовується? З огляду на "хоча це, мабуть, має бути", а не ", хоча воно, мабуть, не повинно" , це, мабуть, стандарт, на який він посилається.
Пітер Мортенсен

2

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


Я думаю, що це невизначена поведінка (див. Кінець), і у Кланг просто одна реалізація. GCC дійсно працює, як ви очікували, оптимізуючи лише unreachableоперацію друку, але залишаючи цикл. Деякі, як Кланг дивно приймає рішення, комбінуючи вкладиш і визначаючи, що це може зробити з циклом.

Поведінка надзвичайно дивна - вона знімає остаточний відбиток, тож "бачить" нескінченну петлю, але потім позбується і циклу.

Наскільки я можу сказати, це ще гірше. Видаляючи вкладиш, ми отримуємо:

die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
  jmp .LBB0_1
main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

тому функція створена, а виклик оптимізований. Це навіть стійкіше, ніж очікувалося:

#include <stdio.h>

void die(int x) {
    while(x);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

призводить до дуже неоптимальної збірки для функції, але виклик функції знову оптимізовано! Навіть гірше:

void die(x) {
    while(x++);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

Я зробив купу іншого тесту, додавши локальну змінну та збільшивши її, передавши вказівник, використовуючи gotoтощо ... На цьому етапі я б відмовився. Якщо потрібно використовувати кланг

static void die() {
    int volatile x = 1;
    while(x);
}

виконує роботу. Це забирає оптимізацію (очевидно) і залишає у надмірному фіналі printf. Принаймні програма не зупиняється. Можливо, GCC зрештою?

Додаток

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

Heck n1528 має такі, як невизначена поведінка, якщо я читаю це право. Конкретно

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

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


Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Bhargav Rao

Re "звичайний все помилка" : Ви маєте на увазі " звичайний старий помилка" ?
Пітер Мортенсен

@PeterMortensen "оле" буде добре і зі мною.
кабанус

2

Здається, що це помилка у компіляторі Clang. Якщо die()функція стати статичною функцією не має жодного примусу , виконайте staticтакі дії inline:

#include <stdio.h>

inline void die(void) {
    while(1)
        ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

Він працює, як очікувалося, при компіляції з компілятором Clang, а також є портативним.

Провідник компілятора (godbolt.org) - кланг 9.0.0-O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB0_1
.Lstr:
        .asciz  "begin"

Про що static inline?
СС Енн

1

Здається, що для мене працює таке:

#include <stdio.h>

__attribute__ ((optnone))
static void die(void) {
    while (1) ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

у Godbolt

Явно зазначає Кланг не оптимізувати, що одна функція викликає нескінченний цикл, який випромінюється, як очікувалося. Сподіваємось, існує спосіб вибіркового відключення конкретних оптимізацій, а не просто їх вимкнення. Кланг все ж відмовляється видавати код на другий printf. Щоб змусити це зробити, мені довелося додатково змінити код всередині main:

volatile int x = 0;
if (x == 0)
    die();

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


1
Не потрібно, щоб другий printfгенерувався, якщо цикл насправді йде назавжди, тому що в цьому випадку другий printfдійсно недоступний і тому може бути видалений. (Помилка Кланг полягає в тому, що виявляє недоступність, а потім видаляє цикл таким чином, щоб досягти недосяжного коду).
nneonneo

GCC документи __attribute__ ((optimize(1))), але clang ігнорує це як непідтримуваний: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Пітер Кордес,

0

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

Я думаю, що намір стандарту є цілком зрозумілим, що компілятори не повинні вважати, що while(1) {}цикл без побічних ефектів і breakвисловлювань не закінчується. На відміну від того, що можуть подумати деякі люди, автори «Стандартів» не запрошували авторів-упорядників бути дурними або тупими. Відповідна реалізація може бути корисною для вирішення питання про припинення будь-якої програми, яка, якщо не буде перервана, виконує більше вільних інструкцій щодо побічних ефектів, ніж є атоми у Всесвіті, але якісна реалізація не повинна виконувати таких дій на основі будь-якого припущення про припинення, а скоріше на тій підставі, що це може бути корисним, і не може (на відміну від поведінки Кланга) бути гіршим, ніж марним.


-2

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

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

Навіть якби Кланг переклав код наївно, ви можете уявити оптимізуючий процесор, який може виконати кожну ітерацію вдвічі менше часу, що минула попередня ітерація. Це буквально завершить нескінченний цикл за обмежений час. Чи порушує такий оптимізуючий процесор стандарт? Здається, досить абсурдно говорити, що оптимізація процесора порушила б стандарт, якщо це занадто добре в оптимізації. Те саме стосується компілятора.


Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

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

@pipe: Я думаю, що керівники clang і gcc сподіваються, що майбутня версія стандарту зробить поведінку їхніх компіляторів допустимою, і технічне обслуговування цих компіляторів зможе зробити вигляд, що така зміна була лише виправленням давнього дефекту у Стандарті. Так, наприклад, вони поставилися до загальних гарантій початкової послідовності C89.
supercat

@SSAnne: Хм ... я не думаю, що цього достатньо, щоб блокувати деякі нерезультативні висновки gcc та clang вивести з результатів порівнянь рівності вказівників.
supercat

@supercat Є <s> інші </s> тонни.
СС Енн

-2

Вибачте, якщо це абсурдно не так, я натрапив на цю посаду і знаю, бо мої роки, що використовують дистрибутив Gentoo Linux, що якщо ви хочете, щоб компілятор не оптимізував ваш код, ви повинні використовувати -O0 (Zero). Мені було цікаво про це, і склав і запустив вищезгаданий код, і цикл do проходить нескінченно. Складено за допомогою clang-9:

cc -O0 -std=c11 test.c -o test

1
Сенс у тому, щоб зробити нескінченний цикл із включеними оптимізаціями.
СС Енн

-4

Порожній whileцикл не має жодних побічних ефектів у системі.

Тому Кланг знімає його. Існують "кращі" способи досягти наміченої поведінки, які змушують вас бути більш очевидними щодо своїх намірів.

while(1); є baaadd.


6
У багатьох вбудованих конструкціях немає поняття abort()або exit(). Якщо виникає ситуація, коли функція визначає, що (можливо, внаслідок пошкодження пам’яті) тривалість виконання буде гіршою, ніж небезпечною, звичайна поведінка за замовчуванням для вбудованих бібліотек полягає у виклику функції, яка виконує a while(1);. Це може бути корисно для компілятора , щоб мати можливість підмінити більш корисне поведінка, але будь-який компілятор письменник , який не може зрозуміти, як ставитися до такої простої конструкції в якості бар'єру для продовження виконання програми нездатний довірити складні оптимізації.
supercat

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

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

... "зачекайте N + 1 секунди, а потім виконайте якусь іншу дію", щоб той факт, що набір допустимих дій, окрім очікування, порожній, не було б помітно. З іншого боку, якщо фрагмент коду видаляє якусь нестерпну дію з набору можливих дій, і одна з цих дій все-таки виконується , це слід вважати спостережуваним. На жаль, мовні правила C та C ++ використовують слово "припустити" дивним чином на відміну від будь-якої іншої сфери логіки чи людських зусиль, які я можу визначити.
supercat

1
@FamousJameis нормально, але Clang не просто видаляє цикл - він статично аналізує все згодом як недоступний і видає недійсну інструкцію. Це не на те, чого ви очікуєте, якщо він просто "зняв" цикл.
nneonneo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.