Разрешено ли компиляторам устранять бесконечные циклы? - PullRequest
27 голосов
/ 01 февраля 2010

Может оптимизирующий компилятор удалять бесконечные циклы, что не меняет никаких данных, например

while(1) 
  /* noop */;

Из анализа компилятора графа потока данных можно сделать вывод, что такой цикл является "мертвым кодом" без каких-либо побочных эффектов.

Запрещено ли удаление бесконечных циклов стандартами C90 / C99?

Разрешают ли стандарты C90 или C99 компилятору удалять такие циклы?

Upd: «Microsoft C версии 6.0, по сути, выполнила эту оптимизацию», см. Ссылку по caf.

label: goto label;
return 0;

будет преобразован в

return 0;

Ответы [ 6 ]

25 голосов
/ 23 февраля 2015

C11 разъясняет ответ на этот вопрос, в черновик стандартного раздела C11 6.8.5 Итерационные заявления добавлен следующий абзац:

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

и сноска 157 гласит:

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

Итак, ваш конкретный пример:

while(1) 
  /* noop */;

не подходит для оптимизации, поскольку управляющее выражение является константным выражением.

Бесконечные циклы как UB

Так почему компиляторам разрешено оптимизировать бесконечные циклы с исключением, приведенным выше, хорошо, Ханс Бём предлагает обоснование для создания неопределенного поведения бесконечных циклов в: N1528: Почему неопределенное поведение для бесконечных циклов? , Следующая цитата дает хорошее представление о проблеме:

Как правильно указывает N1509, текущий проект по существу дает неопределенное поведение бесконечных циклов в 6.8.5p6. Основная проблема для при этом он позволяет коду перемещаться по потенциально бесконечный цикл. Например, предположим, что у нас есть следующие циклы, где count и count2 являются глобальными переменными (или имеют свой адрес берется), а p - локальная переменная, адрес которой не был взят:

for (p = q; p != 0; p = p -> next) {
    ++count;
}
for (p = q; p != 0; p = p -> next) {
    ++count2;
}

Могут ли эти два цикла быть объединены и заменены следующим циклом?

for (p = q; p != 0; p = p -> next) {
        ++count;
        ++count2;
}

Без специального разрешения в 6.8.5p6 для бесконечных циклов это будет запрещено: если первый цикл не заканчивается, потому что д указывает на круговой список, оригинал никогда не пишет в count2. таким образом он может быть запущен параллельно с другим потоком, который обращается или Количество обновлений2. Это больше не безопасно с преобразованной версией который делает доступ к count2, несмотря на бесконечный цикл. Таким образом преобразование потенциально вводит гонку данных.

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

C99

Поскольку C99 не имеет этого исключения, мы обратимся к правилу как если бы , описанному в разделе 5.1.2.3, в котором в основном говорится, что компилятор должен только имитировать наблюдаемое поведение программы Требования следующие:

Минимальные требования к соответствующей реализации:

  • В точках последовательности летучие объекты стабильны в том смысле, что предыдущие обращения полные и последующие обращения еще не произошли.
  • При завершении программы все данные, записанные в файлы, должны быть идентичны результату, который Выполнение программы в соответствии с абстрактной семантикой привело бы к созданию.
  • Динамика ввода и вывода интерактивных устройств осуществляется в соответствии с 7.19.3. Цель этих требований заключается в том, что небуферизованный или линейный буферизованный вывод появиться как можно скорее, чтобы убедиться, что сообщения-подсказки действительно появляются до программа, ожидающая ввода.

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

while(1) ;
printf( "hello world\n" ) ;

Многие утверждают, что воздействие на завершение процесса также является наблюдаемым поведением, эта позиция взята из Компиляторы C опровергают последнюю теорему Ферма :

Компилятору предоставлена ​​значительная свобода в том, как он реализует программу на C, но его выходные данные должны иметь то же внешне видимое поведение, что и программа при интерпретации «абстрактной машиной C», которая описана в стандарте. Многие знающие люди (в том числе и я) считают это тем, что говорят, что поведение при завершении программы не должно изменяться. Очевидно, что некоторые авторы компиляторов не согласны или не верят, что это имеет значение. Тот факт, что разумные люди не согласны с толкованием, может указывать на то, что стандарт С имеет недостатки.

Обновление

Я как-то пропустил продолжение этой статьи, Пересмотренные компиляторы и завершение , в которой говорится следующее о разделе 5.1.2.3:

Второе требование - хитрое. Если речь идет о завершении программы, запущенной на абстрактной машине, то она встречается пусто, потому что наша программа не завершается. Если речь идет об окончании фактической программы, сгенерированной компилятором, то реализация C ошибочна, потому что данные, записанные в файлы (stdout - это файл), отличаются от данных, записываемых абстрактной машиной. (Это чтение из-за Ганса Бема; я не смог дразнить эту тонкость вне стандарта.)

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

Это относится и к бесконечным циклам goto?

Я полагаю, что намерение состоит в том, что это также относится к бесконечным циклам goto. В C ++ это явно так, поскольку в разделе 1.10 [intro.multithread] написано:

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

  • прекратить
  • сделать вызов функции ввода / вывода библиотеки,
  • получить доступ или изменить изменчивый объект, или
  • выполнить операцию синхронизации или атомарную операцию.

и затем намерение, выраженное в N1528, заключается в том, что стандарты C и C ++ согласуются:

Поскольку бэкэнды компилятора обычно совместно используются компиляторами C и C ++, представляется наиболее важным, чтобы WG14 и WG21 согласовали любое принятое решение. Альтернативы могут заключаться в особой обработке двух языков бэк-эндом или отключении оптимизаций при обработке кода C. Ни то, ни другое не кажется желательным.

и в конце говорит:

WG21 рассматривает улучшенную формулировку, которая делает обработку последовательной. Надеемся, что WG14 отследит любые возникающие изменения.

В настоящее время стандарт C11 не содержит аналогичных формулировок в разделе 5.1.2.4 Многопоточные выполнения и гонки данных , но, учитывая N1528, разумно предположить, что компиляторы будут рассматривать бесконечные циклы goto как неопределенное поведение в C и C ++.

Примечание также см. Комментарий США 38 здесь и N3196 , к которым относится бумага, с которой было применено это изменение.

9 голосов
/ 01 февраля 2010

Нет способа универсально обнаружить бесконечные циклы: см. Проблема остановки . Поэтому лучшее, что может сделать любой компилятор, - это сделать приличное предположение - например, очевидный случай, упомянутый в ОП.

Но почему это было бы желательно? Я мог видеть выдачу предупреждения и все еще разрешать поведение, но удалить цикл - это не «оптимизация» - это меняет поведение программы!

8 голосов
/ 01 февраля 2010

Цикл - это не мертвый код, он в основном не позволяет программе достичь того, что последует за ней.Это не то, что произойдет, если цикл был удален, поэтому компилятор не может удалить цикл.

Он может заменить его зависимой от платформы командой ожидания, чтобы сообщить процессору, что поток не собираетсяделать что-либо еще.

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

4 голосов
/ 02 февраля 2010

Это уже много раз обсуждалось на comp.lang.c (например, здесь ), насколько мне известно, без какого-либо консенсуса.

3 голосов
/ 01 февраля 2010

Они необходимы при написании демонов.Почему вы хотите назвать их мертвым кодом?

0 голосов
/ 08 декабря 2010

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

...