Это ошибка Clang
... при встраивании функции, содержащей бесконечный цикл. Поведение отличается, когда while(1);
появляется непосредственно в основном, что пахнет очень глючно для меня.
Смотрите @ Арнавион ответ для резюме и ссылки. Остальная часть этого ответа была написана до того, как я получил подтверждение, что это была ошибка, не говоря уже об известной ошибке.
Чтобы ответить на заглавный вопрос: как сделать бесконечный пустой цикл, который не будет оптимизирован? ? -
сделать die()
макрос, а не функцию , чтобы обойти эту ошибку в Clang 3.9 и более поздних версиях. (Более ранние версии Clang либо сохраняют цикл, либо отправляютcall
в не встроенную версию функции с бесконечным циклом.) Это кажется безопасным, даже если print;while(1);print;
функция встроена в свой вызывающей ( Godbolt ). -std=gnu11
против -std=gnu99
ничего не меняет.
Если вы заботитесь только о GNU C, P__J ____asm__("");
внутри цикла также работает, и не должно мешать оптимизации любого окружающего кода для любых компиляторов, которые его понимают. Базовые asm-операторы GNU C являются неявнымиvolatile
, так что это считается видимым побочным эффектом, который должен «исполняться» столько раз, сколько это было бы в абстрактной машине C. (И да, Clang реализует GNU-диалект C, как описано в руководстве GCC.)
Некоторые люди утверждают, что было бы законно оптимизировать пустой бесконечный цикл. Я не согласен с 1 , но даже если мы примем это, для Clang также не может быть законным предполагать, что операторы после цикла недоступны, и позволить выполнению выпасть из конца функции в следующую функцию или в мусор который декодирует как случайные инструкции.
(Это было бы совместимо со стандартами для Clang ++ (но все же не очень полезно); бесконечные циклы без каких-либо побочных эффектов - это UB в C ++, но не C.
Is while (1); неопределенное поведение в C? UB позволяет компилятору выдавать практически все для кода на пути выполнения, который обязательно встретит UB. asm
Оператор в цикле избежал бы этого UB для C ++. Но на практике компиляция Clang как C ++ не удаляет бесконечные пустые циклы с постоянным выражением, кроме как при встраивании, так же как и при составление как C.)
Встраивание вручную while(1);
меняет компиляцию Clang: бесконечный цикл присутствует в asm. Это то, что мы ожидаем от адвоката правил POV.
#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"
Тот же компилятор с теми же параметрами компилирует main
который infloop() { while(1); }
сначала вызывает тот же puts
, но затем просто прекращает выдавать инструкции main
после этого момента. Итак, как я уже сказал, выполнение просто выпадает из конца функции, в любую функцию, следующую за ней (но со стеком, смещенным для входа в функцию, так что это даже не допустимый вызов вызова).
Действительные варианты будут
- испускать
label: jmp label
бесконечный цикл
- или (если мы допустим, что бесконечный цикл может быть удален) сгенерировать еще один вызов для печати 2-й строки, а затем
return 0
изmain
.
Сбой или иное продолжение без печати «недоступен» явно не подходит для реализации C11, если только не существует UB, который я не заметил.
Сноска 1:
Для справки, я согласен с ответом @ Lundin, который цитирует стандарт для доказательства того, что C11 не допускает допущения завершения для бесконечных циклов с постоянным выражением, даже когда они пусты (без ввода-вывода, энергозависимости, синхронизации или других). видимые побочные эффекты).
Это набор условий, позволяющих скомпилировать цикл в пустой цикл asm. для обычного ЦП. (Даже если тело не было пустым в источнике, назначения переменных не могут быть видны другим потокам или обработчикам сигналов без UB с гонкой данных во время работы цикла. Поэтому соответствующая реализация может удалить такие тела цикла, если она этого хочет Т. к. тогда остается вопрос о том, можно ли удалить сам цикл. ISO C11 явно говорит нет.)
Учитывая, что C11 выделяет этот случай как случай, когда реализация не может предположить, что цикл завершается (и что это не UB), кажется ясным, что они намереваются, чтобы цикл присутствовал во время выполнения. Реализация, нацеленная на процессоры с моделью исполнения, которая не может выполнять бесконечное количество работы за конечное время, не имеет оснований для удаления пустого постоянного бесконечного цикла. Или даже в целом, точная формулировка о том, можно ли «предположительно прекратить» или нет. Если цикл не может завершиться, это означает, что более поздний код недоступен, независимо от того, какие аргументы вы приводите в отношении математики и бесконечности, и сколько времени занимает выполнение бесконечного объема работы на некоторой гипотетической машине.
Кроме того, Clang - это не просто DeathStation 9000, совместимая с ISO C, он предназначен для практического программирования низкоуровневых систем, включая ядра и встроенные компоненты. Поэтому, независимо от того, принимаете ли вы аргументы о том, что C11 разрешает удаление while(1);
, не имеет смысла, что Clang захочет это сделать. Если ты пишешьwhile(1);
, это, вероятно, не было случайностью. Удаление циклов, которые заканчиваются бесконечно случайно (с управляющими выражениями переменных времени выполнения), может быть полезным, и это имеет смысл для компиляторов.
Редко, когда вы хотите просто крутиться до следующего прерывания, но если вы напишите это в C, это определенно то, что вы ожидаете. (А что делает происходит в GCC и Clang, за исключением случаев , когда Clang бесконечный цикл внутри функции - оболочки).
Например, в примитивном ядре ОС, когда у планировщика нет задач для запуска, он может запустить незанятую задачу. Первая реализация этого может быть while(1);
.
Или для оборудования без какой-либо функции энергосбережения, которая может быть единственной реализацией. (До начала 2000-х это было, я думаю, нередко на x86. Хотя hlt
инструкция существовала, IDK, если она сохраняла значительный объем энергии до тех пор, пока центральные процессоры не начали переходить в режим ожидания с низким энергопотреблением.)