Как сделать бесконечный пустой цикл, который не будет оптимизирован?


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 .

Было удивительно трудной задачей заставить 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 выводить правильный бесконечный цикл без доступа к памяти с включенной оптимизацией?


3
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Бхаргав Рао

2
Там нет портативного решения, которое не связано с побочным эффектом. Если вам не нужен доступ к памяти, вам лучше всего зарегистрировать volatile неподписанный символ; но регистрация уходит в C ++ 17.
Скотт М

25
Может быть, это не в рамках вопроса, но мне любопытно, почему вы хотите это сделать. Конечно, есть какой-то другой способ выполнить вашу реальную задачу. Или это просто академический характер?
Cruncher

1
@Cruncher: последствия любой конкретной попытки запуска программы могут быть полезными, практически бесполезными или значительно хуже, чем бесполезными. Выполнение, в результате которого программа застревает в бесконечном цикле, может быть бесполезным, но все же предпочтительнее, чем другое поведение, которое может заменить компилятор.
суперкат

6
@Cruncher: потому что код может быть запущен в автономном контексте, в котором отсутствует понятие exit(), и потому что код мог обнаружить ситуацию, когда он не может гарантировать, что последствия продолжительного выполнения будут не хуже, чем бесполезными . Цикл перехода к самому себе - довольно паршивый способ справиться с такими ситуациями, но, тем не менее, он может быть лучшим способом справиться с плохой ситуацией.
суперкат

Ответы:


77

Стандарт C11 говорит об этом, 6.8.5 / 6:

Оператор итерации, управляющее выражение которого не является константным выражением, 156), который не выполняет никаких операций ввода-вывода, не обращается к изменчивым объектам и не выполняет никаких синхронизирующих или атомарных операций в своем теле, управляющем выражении или (в случае для for утверждение) его выражение-3, может быть принято реализацией для прекращения. 157)

Примечания к двум стопам не являются нормативными, но содержат полезную информацию:

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

157) Это предназначено для разрешения преобразований компилятора, таких как удаление пустых циклов, даже если завершение не может быть доказано.

В вашем случае while(1)это кристально чистое постоянное выражение, поэтому оно может не допустить его завершение. Такая реализация была бы безнадежно нарушена, поскольку циклы "навсегда" - это обычная программная конструкция.

Однако то, что происходит с «недоступным кодом» после цикла, насколько я знаю, недостаточно четко определено. Тем не менее, Clang действительно ведет себя очень странно. Сравнение машинного кода с 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 генерирует цикл, clang просто бежит в лес и выходит с ошибкой 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)и остановиться на этом, только печатая «begin» (при условии \ n сбрасывает стандартный вывод). Это происходит с GCC.

Если цикл был просто удален, он должен напечатать «begin» 2 раза, а затем «unreachable». Однако на clang ( godbolt ) он печатает «begin» 1 раз, а затем «unreachable» перед возвратом кода выхода 0. Это просто неправильно, независимо от того, как вы это выразили.

Я не могу найти ни одного случая, чтобы заявлять о неопределенном поведении, поэтому я считаю, что это ошибка в clang. В любом случае, такое поведение делает clang на 100% бесполезным для таких программ, как встроенные системы, где вы просто должны быть в состоянии полагаться на вечные циклы, подвешивающие программу (в ожидании сторожевого таймера и т. Д.).


15
Я не согласен с «этим кристально чистым константным выражением, поэтому реализация может не допустить его завершение» . Это действительно относится к адвокатской деятельности, основанной на придирчивом языке, но 6.8.5/6в форме если (эти), то вы можете предположить (это) . Это не означает, что если нет (эти), вы можете не предполагать (это) . Это спецификация только для тех случаев, когда выполняются условия, а не тогда, когда они неудовлетворены, когда вы можете делать все, что хотите в соответствии со стандартами. И если нет наблюдаемых ...
Кабанус

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

В других ответах уже рассказывалось, как заставить Clang создавать бесконечный цикл с использованием встроенного языка ассемблера или других побочных эффектов. Я просто хочу подтвердить, что это действительно ошибка компилятора. В частности, это давняя ошибка LLVM - она ​​применяет концепцию C ++ «все циклы без побочных эффектов должны заканчиваться» к языкам, где это не должно, например, C.

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

В краткосрочной перспективе кажется, что LLVM будет продолжать предполагать, что «все петли без побочных эффектов должны завершаться». Для любого языка, который допускает бесконечные циклы, LLVM ожидает, что клиентский интерфейс вставит llvm.sideeffectкоды операций в такие циклы. Это то, что Rust планирует сделать, поэтому Clang (при компиляции кода на C), вероятно, тоже должен будет это сделать.


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

4
@IanKemp: чтобы исправить ошибку сейчас, нужно признать, что на исправление ошибки ушло десять лет. Лучше оставить надежду, что Стандарт изменится, чтобы оправдать их поведение. Конечно, даже если стандарт изменился, это все равно не оправдало бы их поведение, за исключением тех людей, которые расценили бы изменение стандарта как указание на то, что предыдущий поведенческий мандат стандарта был дефектом, который следует исправить задним числом.
суперкат

4
Это было «исправлено» в том смысле, что LLVM добавил sideeffectоперацию (в 2017 году) и ожидает, что внешние интерфейсы будут вставлять эту операцию в циклы по своему усмотрению. LLVM должен был выбрать какой-то цикл по умолчанию, и он выбрал тот, который соответствует намеренно или иным образом поведению C ++. Конечно, еще предстоит проделать определенную работу по оптимизации, например объединить последовательные sideeffectоперации в одну. (Это то, что блокирует внешний интерфейс Rust от его использования.) Таким образом, на этом основании ошибка находится во внешнем интерфейсе (лязг), который не вставляет операцию в циклы.
Арнавион

@Arnavion: Есть ли способ указать, что операции могут быть отложены до тех пор, пока не будут использованы результаты, но если данные приведут к бесконечному циклу выполнения программы, попытка выполнить прошлые зависимости данных сделает программу хуже, чем бесполезной ? Необходимость добавить фальшивые побочные эффекты, которые бы помешали прежним полезным оптимизациям, чтобы не дать оптимизатору сделать программу хуже бесполезной, не похоже на рецепт эффективности.
суперкат

Это обсуждение, вероятно, принадлежит спискам рассылки LLVM / clang. FWIW коммит LLVM, который добавил опцию, также научил нескольким проходам оптимизации об этом. Кроме того, Rust экспериментировал со вставкой sideeffectопций в начало каждой функции и не видел никакого снижения производительности во время выполнения. Единственная проблема - это регрессия времени компиляции , по-видимому, из-за отсутствия слияния последовательных операций, как я упоминал в моем предыдущем комментарии.
Арнавион

32

Это ошибка 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, если она сохраняла значительный объем энергии до тех пор, пока центральные процессоры не начали переходить в режим ожидания с низким энергопотреблением.)


1
Из любопытства кто-нибудь на самом деле использует Clang для встроенных систем? Я никогда не видел это, и я работаю исключительно со встроенными. gcc только «недавно» (10 лет назад) вышел на рынок встраиваемых систем, и я использую его скептически, желательно с низкой оптимизацией и всегда с -ffreestanding -fno-strict-aliasing. Он отлично работает с ARM и, возможно, с устаревшим AVR.
Лундин

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

2
bugs.llvm.org/show_bug.cgi?id=965 эта ошибка выглядит актуальной, но я не уверен, что это то, что мы видим здесь.
Bracco23

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

1
@JeffLearman Недавно стал мейнстримом? В любом случае, фиаско gcc со строгим псевдонимом произошло только после появления C99, и более новые его версии, похоже, больше не будут бананами, когда сталкиваются со строгими нарушениями псевдонимов. Тем не менее, я остаюсь скептиком, когда использую его. Что касается Clang, последняя версия, очевидно, полностью сломана, когда речь идет о вечных циклах, поэтому она не может быть использована для встроенных систем.
Лундин

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 так настаивали на своем праве производить низкокачественную реализацию, но Стандарт это допускает.
суперкат

2
@supercat спасибо за комментарий ... почему превышение лимита перевода может сделать что-то кроме отказа фазы перевода и отказаться от выполнения? Также: « 5.1.1.3 Диагностика Соответствующая реализация должна выдавать ... диагностическое сообщение ... если блок преобразования или блок предварительной обработки содержит нарушение какого- либо синтаксического правила или ограничения ...». Я не вижу, как ошибочное поведение на этапе исполнения может когда-либо соответствовать.
Джонатанджо

Стандарт был бы совершенно невозможен для реализации, если бы все пределы реализации должны были быть разрешены во время сборки, так как можно было бы написать строго согласованную программу, которая потребовала бы больше байтов стека, чем атомов во вселенной. Неясно, следует ли смешивать ограничения времени выполнения с «пределами перевода», но такая уступка явно необходима, и нет другой категории, в которую она может быть помещена.
суперкат

1
Я отвечал на ваш комментарий о «пределах перевода». Конечно, существуют также ограничения на исполнение, я признаюсь, я не понимаю, почему вы предлагаете, чтобы они были смешаны с ограничениями перевода или почему вы говорите, что это необходимо. Я просто не вижу причин, по которым nasty: goto nastyможно сказать, что они соответствуют и не вращают ЦП, пока не вмешается пользователь или ресурс.
Джонатанджо

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

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();
}

быть пропущенным. Интересно, что Clang пропускает это, а gcc - нет .


«Это ничего не говорит о том, что происходит, если критерии не выполняются». Но это так, 6.8.5.1. Оператор while: «Оценка управляющего выражения выполняется перед каждым выполнением тела цикла». Вот и все. Это вычисление значения (константного выражения), оно подпадает под правило абстрактной машины 5.1.2.3, которое определяет термин «оценка»: « Оценка выражения в целом включает как вычисления значений, так и инициирование побочных эффектов». И в соответствии с той же главой, все такие оценки секвенируются и оцениваются в соответствии с семантикой.
Лундин

1
@Lundin Так while(1){}бесконечная последовательность 1оценок переплетается с {}оценками, но где в стандарте говорится, что эти оценки должны занимать ненулевое время? Поведение gcc более полезно, я думаю, потому что вам не нужны приемы, связанные с доступом к памяти, или приемы вне языка. Но я не уверен, что стандарт запрещает эту оптимизацию в Clang. Если while(1){}намерение сделать неоптимизируемым является намерением, в стандарте должно быть четко указано об этом, а бесконечный цикл должен быть указан как наблюдаемый побочный эффект в 5.1.2.3p2.
PSkocik

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

2
+1 Даже если мне кажется, что «выполнение зависает бесконечно без какого-либо вывода» - это «побочный эффект» в любом определении «побочного эффекта», который имеет смысл и полезен не только в условиях вакуума, это помогает объяснить мышление, из которого это может иметь смысл для кого-то.
mtraceur

1
Почти "оптимизация бесконечного цикла" : не совсем ясно, относится ли "оно" к стандарту или компилятору - возможно, перефразировать? Учитывая, «хотя это, вероятно, должно», а не «хотя, вероятно, не должно» , это, вероятно, стандарт, к которому относится «это» .
Питер Мортенсен

2

Я был убежден, что это просто старая ошибка. Я оставляю свои тесты ниже и, в частности, ссылку на обсуждение в стандартном комитете по некоторым причинам, которые у меня были ранее.


Я думаю, что это неопределенное поведение (см. Конец), и у Clang есть только одна реализация. GCC действительно работает, как вы ожидаете, оптимизируя только unreachableоператор печати, но оставляя цикл. Кое-что, как Clang странным образом принимает решения, комбинируя встраивание и определяя, что он может делать с циклом.

Поведение является очень странным - оно удаляет окончательный отпечаток, поэтому «видит» бесконечный цикл, но затем избавляется от цикла.

Это даже хуже, насколько я могу судить. Извлекая строку, получаем:

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");
}

Я сделал кучу других тестов, добавив локальную переменную и увеличив ее, передав указатель, используя gotoetc и т. Д. В этот момент я бы сдался. Если вы должны использовать Clang

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

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

добавление

После обсуждения с Дэвидом я пришел к выводу, что в стандарте не говорится «если условие постоянное, вы не можете предполагать, что цикл завершается». Таким образом, и, учитывая, что в стандарте нет наблюдаемого поведения (как определено в стандарте), я бы поспорил только о согласованности - если компилятор оптимизирует цикл, поскольку он предполагает, что он завершается, он не должен оптимизировать следующие операторы.

Heck n1528 имеет неопределенное поведение, если я правильно понял . конкретно

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

Отсюда я думаю, что это может перейти только к обсуждению того, что мы хотим (ожидаемо?), А не того, что разрешено.


Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Бхаргав Рао

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) - clang 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?
SS Anne

1

Следующее, кажется, работает для меня:

#include <stdio.h>

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

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

на кресте

Явное указание Clang не оптимизировать эту одну функцию приводит к тому, что бесконечный цикл запускается, как и ожидалось. Надеемся, что есть способ выборочно отключить определенные оптимизации вместо того, чтобы просто отключить их все так. Clang по-прежнему отказывается выдавать код для второго printf, хотя. Чтобы заставить это сделать это, мне пришлось дополнительно изменить код внутриmain чтобы:

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

Похоже, вам нужно отключить оптимизацию для функции бесконечного цикла, а затем убедиться, что ваш бесконечный цикл вызывается условно. В реальном мире последнее почти всегда так или иначе.


1
Нет необходимости printfгенерировать второе , если цикл действительно работает вечно, потому что в этом случае второе printfдействительно недоступно и, следовательно, может быть удалено. (Ошибка Clang заключается в обнаружении недоступности и последующем удалении цикла, так что достигается недоступный код).
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

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

Компилятору разрешено оптимизировать все, что не является наблюдаемым поведением, как определено в стандарте. Это включает время выполнения. Не требуется сохранять тот факт, что цикл, если он не оптимизирован, будет занимать бесконечное количество времени. Разрешается изменить это на гораздо более короткое время выполнения - фактически, это точка большинства оптимизаций. Ваш цикл был оптимизирован.

Даже если clang наивно переводит код, вы можете себе представить оптимизирующий процессор, который может завершить каждую итерацию за половину времени, которое занимало предыдущая итерация. Это буквально завершило бы бесконечный цикл за конечное время. Такой оптимизирующий процессор нарушает стандарт? Кажется абсурдным сказать, что оптимизирующий процессор будет нарушать стандарт, если он слишком хорош в оптимизации. То же самое относится и к компилятору.


Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Самуэль Лев

4
Исходя из вашего опыта (из вашего профиля), я могу только сделать вывод, что этот пост написан недобросовестно только для защиты компилятора. Вы серьезно утверждаете, что то, что занимает бесконечное количество времени, может быть оптимизировано для выполнения за половину времени. Это смешно на каждом уровне, и вы это знаете.
труба

@pipe: Я думаю, что сопровождающие clang и gcc надеются, что будущая версия стандарта сделает поведение их компиляторов допустимым, и сопровождающие этих компиляторов смогут сделать вид, что такое изменение было просто исправлением давнего дефекта. в стандарте. Вот как, например, они относились к гарантиям Common Initial Sequence C89.
суперкат

@SSAnne: Хм ... Я не думаю, что этого достаточно, чтобы заблокировать некоторые неправильные выводы, полученные gcc и clang из результатов сравнений на равенство указателей.
суперкат

@supercat Есть <s> другие </ s> тонны.
SS Anne

-2

Извините, если это абсурдно не так, я наткнулся на этот пост, и я знаю, потому что мои годы использования дистрибутива Gentoo Linux, что если вы хотите, чтобы компилятор не оптимизировал ваш код, вы должны использовать -O0 (ноль). Мне было любопытно, и я скомпилировал и запустил приведенный выше код, и цикл выполнения идет бесконечно. Скомпилировано с использованием clang-9:

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

1
Смысл в том, чтобы сделать бесконечный цикл с включенной оптимизацией.
С.С. Энн

-4

Пустой whileцикл не имеет побочных эффектов в системе.

Поэтому Clang удаляет его. Существуют «лучшие» способы достижения намеченного поведения, которые заставляют вас быть более очевидными в своих намерениях.

while(1); Бааадд.


6
Во многих встроенных конструкциях отсутствует понятие abort()или exit(). Если возникает ситуация, когда функция определяет, что (возможно, в результате повреждения памяти) продолжение выполнения будет хуже, чем опасно, обычным поведением по умолчанию для встроенных библиотек является вызов функции, выполняющей a while(1);. Для компилятора может быть полезно иметь варианты для замены более полезного поведения, но любой автор компилятора, который не может понять, как рассматривать такую ​​простую конструкцию как барьер для продолжения выполнения программы, не может доверять сложной оптимизации.
суперкат

Есть ли способ, которым вы можете быть более явными из ваших намерений? оптимизатор предназначен для оптимизации вашей программы, а удаление лишних циклов, которые ничего не делают, является оптимизацией. это действительно философское различие между абстрактным мышлением мира математики и мира прикладных разработок.
Знаменитый Джейм

У большинства программ есть набор полезных действий, которые они должны выполнять, когда это возможно, и набор действий, которые хуже бесполезных, которые они никогда не должны выполнять ни при каких обстоятельствах. Многие программы имеют набор приемлемых поведений в каждом конкретном случае, одна из которых, если время выполнения не наблюдается, всегда будет «ждать некоторого произвольного и затем выполнять какое-то действие из набора». Если бы все действия, кроме ожидания, были в наборе действий хуже, чем бесполезных, не было бы количества секунд N, для которых «ждать вечно» было бы
заметно

... «подождите N + 1 секунду и затем выполните какое-то другое действие», поэтому тот факт, что набор допустимых действий, кроме ожидания, пуст, не будет заметен. С другой стороны, если часть кода удаляет какое-то недопустимое действие из набора возможных действий, и одно из этих действий все равно выполняется , это следует считать наблюдаемым. К сожалению, правила языка C и C ++ используют слово «предположить» странным образом, в отличие от любой другой области логики или человеческой деятельности, которую я могу идентифицировать.
суперкат

1
@FamousJameis хорошо, но Clang не просто удаляет цикл - он статически анализирует все впоследствии как недостижимое и выдает неверную инструкцию. Это не то, что вы ожидаете, если он просто «удалил» цикл.
nneonneo
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.