Стандарт 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
.
Было удивительно трудной задачей заставить Clang вывести бесконечный цикл -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 выводить правильный бесконечный цикл без доступа к памяти с включенной оптимизацией?
источник
exit()
, и потому что код мог обнаружить ситуацию, когда он не может гарантировать, что последствия продолжительного выполнения будут не хуже, чем бесполезными . Цикл перехода к самому себе - довольно паршивый способ справиться с такими ситуациями, но, тем не менее, он может быть лучшим способом справиться с плохой ситуацией.Ответы:
Стандарт C11 говорит об этом, 6.8.5 / 6:
Примечания к двум стопам не являются нормативными, но содержат полезную информацию:
В вашем случае
while(1)
это кристально чистое постоянное выражение, поэтому оно может не допустить его завершение. Такая реализация была бы безнадежно нарушена, поскольку циклы "навсегда" - это обычная программная конструкция.Однако то, что происходит с «недоступным кодом» после цикла, насколько я знаю, недостаточно четко определено. Тем не менее, Clang действительно ведет себя очень странно. Сравнение машинного кода с gcc (x86):
GCC 9,2
-O3 -std=c11 -pedantic-errors
лязг 9.0.0
-O3 -std=c11 -pedantic-errors
gcc генерирует цикл, clang просто бежит в лес и выходит с ошибкой 255.
Я склоняюсь к тому, чтобы быть несоответствующим поведению лязга. Потому что я попытался расширить ваш пример так:
Я добавил C11,
_Noreturn
чтобы помочь компилятору в дальнейшем. Должно быть ясно, что эта функция будет зависать только от этого ключевого слова.setjmp
вернет 0 при первом выполнении, так что эта программа должна просто врезаться вwhile(1)
и остановиться на этом, только печатая «begin» (при условии \ n сбрасывает стандартный вывод). Это происходит с GCC.Если цикл был просто удален, он должен напечатать «begin» 2 раза, а затем «unreachable». Однако на clang ( godbolt ) он печатает «begin» 1 раз, а затем «unreachable» перед возвратом кода выхода 0. Это просто неправильно, независимо от того, как вы это выразили.
Я не могу найти ни одного случая, чтобы заявлять о неопределенном поведении, поэтому я считаю, что это ошибка в clang. В любом случае, такое поведение делает clang на 100% бесполезным для таких программ, как встроенные системы, где вы просто должны быть в состоянии полагаться на вечные циклы, подвешивающие программу (в ожидании сторожевого таймера и т. Д.).
источник
6.8.5/6
в форме если (эти), то вы можете предположить (это) . Это не означает, что если нет (эти), вы можете не предполагать (это) . Это спецификация только для тех случаев, когда выполняются условия, а не тогда, когда они неудовлетворены, когда вы можете делать все, что хотите в соответствии со стандартами. И если нет наблюдаемых ...int z=3; int y=2; int x=1; printf("%d %d\n", x, z);
нет2
в сборке, так и в пустой бесполезный смыслx
не был назначен после того,y
но после того, какz
из - за оптимизации. Итак, исходя из вашего последнего предложения, мы следуем обычным правилам, предполагаем, что время остановлено (потому что мы не были ограничены ничем лучше), и оставлено в окончательной «недоступной» печати. Теперь мы оптимизируем это бесполезное утверждение (потому что мы не знаем ничего лучше).while(1);
то же самое, что иint y = 2;
заявление о том, какую семантику мы можем оптимизировать, даже если их логика остается в источнике. С n1528 у меня сложилось впечатление, что они могут быть одинаковыми, но, поскольку люди, более опытные, чем я, спорят по-другому, и это, очевидно, официальный баг, тогда за пределами философских дебатов о том, является ли формулировка в стандарте явной Аргумент представляется спорным.Вам нужно вставить выражение, которое может вызвать побочный эффект.
Самое простое решение:
Годболт ссылка
источник
asm("")
неявно,asm volatile("");
и, следовательно, оператор asm должен запускаться столько раз, сколько он выполняется на абстрактной машине gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html . (Обратите внимание , что это не безопасно для его побочные эффекты включают в себя любую память или регистры, вам нужно Extended ассемблер с"memory"
CLOBBER , если вы хотите прочитать или запись в память , что вы когда - либо доступ из C. Basic ассемблере безопасен только такие вещи , какasm("mfence")
илиcli
.)В других ответах уже рассказывалось, как заставить Clang создавать бесконечный цикл с использованием встроенного языка ассемблера или других побочных эффектов. Я просто хочу подтвердить, что это действительно ошибка компилятора. В частности, это давняя ошибка LLVM - она применяет концепцию C ++ «все циклы без побочных эффектов должны заканчиваться» к языкам, где это не должно, например, C.
Например, язык программирования Rust также допускает бесконечные циклы и использует LLVM в качестве бэкэнда, и у него есть такая же проблема.
В краткосрочной перспективе кажется, что LLVM будет продолжать предполагать, что «все петли без побочных эффектов должны завершаться». Для любого языка, который допускает бесконечные циклы, LLVM ожидает, что клиентский интерфейс вставит
llvm.sideeffect
коды операций в такие циклы. Это то, что Rust планирует сделать, поэтому Clang (при компиляции кода на C), вероятно, тоже должен будет это сделать.источник
sideeffect
операцию (в 2017 году) и ожидает, что внешние интерфейсы будут вставлять эту операцию в циклы по своему усмотрению. LLVM должен был выбрать какой-то цикл по умолчанию, и он выбрал тот, который соответствует намеренно или иным образом поведению C ++. Конечно, еще предстоит проделать определенную работу по оптимизации, например объединить последовательныеsideeffect
операции в одну. (Это то, что блокирует внешний интерфейс Rust от его использования.) Таким образом, на этом основании ошибка находится во внешнем интерфейсе (лязг), который не вставляет операцию в циклы.sideeffect
опций в начало каждой функции и не видел никакого снижения производительности во время выполнения. Единственная проблема - это регрессия времени компиляции , по-видимому, из-за отсутствия слияния последовательных операций, как я упоминал в моем предыдущем комментарии.Это ошибка 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.В проводнике компилятора Godbolt Clang 9.0 -O3 компилируется как C (
-xc
) для x86-64:Тот же компилятор с теми же параметрами компилирует
main
которыйinfloop() { while(1); }
сначала вызывает тот жеputs
, но затем просто прекращает выдавать инструкцииmain
после этого момента. Итак, как я уже сказал, выполнение просто выпадает из конца функции, в любую функцию, следующую за ней (но со стеком, смещенным для входа в функцию, так что это даже не допустимый вызов вызова).Действительные варианты будут
label: jmp label
бесконечный цикл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, если она сохраняла значительный объем энергии до тех пор, пока центральные процессоры не начали переходить в режим ожидания с низким энергопотреблением.)источник
-ffreestanding -fno-strict-aliasing
. Он отлично работает с ARM и, возможно, с устаревшим AVR.Для справки, Кланг также плохо себя ведет
goto
:Он выдает тот же результат, что и в вопросе, а именно:
Я вижу, не вижу никакого способа прочитать это, как это разрешено в C11, который говорит только:
Как
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, вы получаете довольно хорошо:
Флаги:
-g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c
источник
nasty: goto nasty
можно сказать, что они соответствуют и не вращают ЦП, пока не вмешается пользователь или ресурс.bar()
пределахfoo()
обрабатываются как вызов от__1foo
до__2bar
, от__2foo
до__3bar
, и т. д. и от__16foo
к__launch_nasal_demons
, которые затем позволят статически распределять все автоматические объекты и превратят то, что обычно является «временем выполнения», в ограничение трансляции.Я сыграю адвоката дьявола и утверждаю, что стандарт не запрещает компилятору оптимизировать бесконечный цикл.
Давайте разберем это. Можно предположить, что оператор итерации, который удовлетворяет определенным критериям, завершается:
Это ничего не говорит о том, что происходит, если критерии не выполняются, и допущение, что цикл может завершиться, даже тогда, когда это явно не запрещено, пока соблюдаются другие правила стандарта.
do { } while(0)
илиwhile(0){}
после всех итерационных операторов (циклов), которые не удовлетворяют критериям, которые позволяют компилятору просто предполагать, что они завершаются, и, тем не менее, они явно завершаются.Но может ли компилятор просто оптимизировать
while(1){}
?5.1.2.3p4 говорит:
Здесь упоминаются выражения, а не утверждения, так что это не на 100% убедительно, но, безусловно, допускает такие вызовы:
быть пропущенным. Интересно, что Clang пропускает это, а gcc - нет .
источник
while(1){}
бесконечная последовательность1
оценок переплетается с{}
оценками, но где в стандарте говорится, что эти оценки должны занимать ненулевое время? Поведение gcc более полезно, я думаю, потому что вам не нужны приемы, связанные с доступом к памяти, или приемы вне языка. Но я не уверен, что стандарт запрещает эту оптимизацию в Clang. Еслиwhile(1){}
намерение сделать неоптимизируемым является намерением, в стандарте должно быть четко указано об этом, а бесконечный цикл должен быть указан как наблюдаемый побочный эффект в 5.1.2.3p2.1
условие как вычисление значения. Время выполнения не имеет значения - важно то, что неwhile(A){} B;
может быть полностью оптимизировано, не оптимизировано и не повторено . Чтобы процитировать абстрактную машину C11, подчеркните: «Наличие точки последовательности между оценками выражений A и B подразумевает, что каждое вычисление значения и побочный эффект, связанный с A, секвенируются перед каждым вычислением значения и побочным эффектом, связанным с B ». Значение явно используется (в цикле).B;
B; while(A){}
A
Я был убежден, что это просто старая ошибка. Я оставляю свои тесты ниже и, в частности, ссылку на обсуждение в стандартном комитете по некоторым причинам, которые у меня были ранее.
Я думаю, что это неопределенное поведение (см. Конец), и у Clang есть только одна реализация. GCC действительно работает, как вы ожидаете, оптимизируя только
unreachable
оператор печати, но оставляя цикл. Кое-что, как Clang странным образом принимает решения, комбинируя встраивание и определяя, что он может делать с циклом.Поведение является очень странным - оно удаляет окончательный отпечаток, поэтому «видит» бесконечный цикл, но затем избавляется от цикла.
Это даже хуже, насколько я могу судить. Извлекая строку, получаем:
поэтому функция создана, а вызов оптимизирован. Это еще более устойчиво, чем ожидалось:
приводит к очень неоптимальной сборке для функции, но вызов функции снова оптимизирован! Еще хуже:
Я сделал кучу других тестов, добавив локальную переменную и увеличив ее, передав указатель, используя
goto
etc и т. Д. В этот момент я бы сдался. Если вы должны использовать Clangделает работу Это отстой при оптимизации (очевидно), и уходит в избыточный финал
printf
. По крайней мере, программа не останавливается. Может быть, GCC в конце концов?добавление
После обсуждения с Дэвидом я пришел к выводу, что в стандарте не говорится «если условие постоянное, вы не можете предполагать, что цикл завершается». Таким образом, и, учитывая, что в стандарте нет наблюдаемого поведения (как определено в стандарте), я бы поспорил только о согласованности - если компилятор оптимизирует цикл, поскольку он предполагает, что он завершается, он не должен оптимизировать следующие операторы.
Heck n1528 имеет неопределенное поведение, если я правильно понял . конкретно
Отсюда я думаю, что это может перейти только к обсуждению того, что мы хотим (ожидаемо?), А не того, что разрешено.
источник
Кажется, это ошибка в компиляторе Clang. Если в
die()
функции нет принуждения быть статической функцией, покончите с этимstatic
и сделайте этоinline
:Он работает как положено при компиляции с помощью компилятора Clang и также переносим.
Проводник компилятора (godbolt.org) - clang 9.0.0
-O3 -std=c11 -pedantic-errors
источник
static inline
?Следующее, кажется, работает для меня:
на кресте
Явное указание Clang не оптимизировать эту одну функцию приводит к тому, что бесконечный цикл запускается, как и ожидалось. Надеемся, что есть способ выборочно отключить определенные оптимизации вместо того, чтобы просто отключить их все так. Clang по-прежнему отказывается выдавать код для второго
printf
, хотя. Чтобы заставить это сделать это, мне пришлось дополнительно изменить код внутриmain
чтобы:Похоже, вам нужно отключить оптимизацию для функции бесконечного цикла, а затем убедиться, что ваш бесконечный цикл вызывается условно. В реальном мире последнее почти всегда так или иначе.
источник
printf
генерировать второе , если цикл действительно работает вечно, потому что в этом случае второеprintf
действительно недоступно и, следовательно, может быть удалено. (Ошибка Clang заключается в обнаружении недоступности и последующем удалении цикла, так что достигается недоступный код).__attribute__ ((optimize(1)))
, но clang игнорирует их как неподдерживаемые: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.htmlСоответствующая реализация может, и многие практические, накладывают произвольные ограничения на то, как долго может выполняться программа или сколько инструкций она будет выполнять, и вести себя произвольно, если эти ограничения нарушаются или - по правилу «как если» - если он определяет, что они неизбежно будут нарушены. При условии, что реализация может успешно обрабатывать, по крайней мере, одну программу, которая номинально использует все ограничения, перечисленные в N1570 5.2.4.1, не выходя за пределы ограничений перевода, наличие ограничений, степень их документирования и последствия их превышения все вопросы качества выполнения за пределами юрисдикции стандарта.
Я думаю, что намерение Стандарта совершенно ясно, что компиляторы не должны предполагать, что
while(1) {}
цикл без побочных эффектов илиbreak
утверждений не прекратится. Вопреки тому, что некоторые люди думают, авторы Стандарта не приглашали авторов компиляторов быть глупыми или тупыми. Соответствующая реализация может быть полезна для принятия решения о прекращении работы любой программы, которая, если не прервана, выполнит больше инструкций, свободных от побочных эффектов, чем атомов во вселенной, но качественная реализация не должна выполнять такие действия на основе какого-либо предположения о прекращение, а скорее на том основании, что это может быть полезно и не будет (в отличие от поведения Кланга) хуже, чем бесполезным.источник
Цикл не имеет побочных эффектов, поэтому его можно оптимизировать. Цикл фактически представляет собой бесконечное число итераций с нулевыми единицами работы. Это не определено ни в математике, ни в логике, и в стандарте не говорится, разрешена ли реализация для выполнения бесконечного числа вещей, если каждая вещь может быть выполнена за нулевое время. Интерпретация Кланга совершенно разумна, когда бесконечное время равно нулю, а не бесконечности. Стандарт не говорит, может ли бесконечный цикл завершиться, если вся работа в циклах фактически завершена.
Компилятору разрешено оптимизировать все, что не является наблюдаемым поведением, как определено в стандарте. Это включает время выполнения. Не требуется сохранять тот факт, что цикл, если он не оптимизирован, будет занимать бесконечное количество времени. Разрешается изменить это на гораздо более короткое время выполнения - фактически, это точка большинства оптимизаций. Ваш цикл был оптимизирован.
Даже если clang наивно переводит код, вы можете себе представить оптимизирующий процессор, который может завершить каждую итерацию за половину времени, которое занимало предыдущая итерация. Это буквально завершило бы бесконечный цикл за конечное время. Такой оптимизирующий процессор нарушает стандарт? Кажется абсурдным сказать, что оптимизирующий процессор будет нарушать стандарт, если он слишком хорош в оптимизации. То же самое относится и к компилятору.
источник
Извините, если это абсурдно не так, я наткнулся на этот пост, и я знаю, потому что мои годы использования дистрибутива Gentoo Linux, что если вы хотите, чтобы компилятор не оптимизировал ваш код, вы должны использовать -O0 (ноль). Мне было любопытно, и я скомпилировал и запустил приведенный выше код, и цикл выполнения идет бесконечно. Скомпилировано с использованием clang-9:
источник
Пустой
while
цикл не имеет побочных эффектов в системе.Поэтому Clang удаляет его. Существуют «лучшие» способы достижения намеченного поведения, которые заставляют вас быть более очевидными в своих намерениях.
while(1);
Бааадд.источник
abort()
илиexit()
. Если возникает ситуация, когда функция определяет, что (возможно, в результате повреждения памяти) продолжение выполнения будет хуже, чем опасно, обычным поведением по умолчанию для встроенных библиотек является вызов функции, выполняющей awhile(1);
. Для компилятора может быть полезно иметь варианты для замены более полезного поведения, но любой автор компилятора, который не может понять, как рассматривать такую простую конструкцию как барьер для продолжения выполнения программы, не может доверять сложной оптимизации.