О CC: Должно ли неопределенное поведение переполнений сохранять логическую согласованность? - PullRequest
0 голосов
/ 22 января 2020

Следующий код производит странные вещи в моей системе:

#include <stdio.h>

void f (int x) {
  int y = x + x;
  int v = !y;
  if (x == (1 << 31))
    printf ("y: %d, !y: %d\n", y, !y);
}

int main () {
  f (1 << 31);
  return 0;
}

Скомпилировано с -O1, это печатает y: 0, !y: 0.

Теперь за пределами удивительного факта, что удаление int v или строки if дают ожидаемый результат, меня не устраивает неопределенное поведение переполнений, переводящих в логическую несогласованность.

Если это считается ошибкой, или философия команды G CC заключается в том, что одно неожиданное поведение может перейти в логическое противоречие?

Ответы [ 4 ]

7 голосов
/ 22 января 2020

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

Стоит ли это считать ошибкой, или философия команды G CC гласит, что одно неожиданное поведение может привести к логическому противоречию?

Это не ошибка. Я не очень разбираюсь в философии команды G CC, но в целом неопределенное поведение «полезно» разработчикам компилятора для реализации определенных оптимизаций: допущение того, что что-то никогда не произойдет, упрощает оптимизацию кода. Причина, по которой все может случиться после UB, именно из-за этого. Компилятор делает много предположений, и если какое-либо из них будет нарушено, тогда исходящему коду нельзя доверять.

Как я уже сказал в другой мой ответ :

Неопределенное поведение означает, что все может произойти. Нет объяснения того, почему что-то странное происходит после вызова неопределенного поведения, и не должно быть . Компилятор может очень хорошо генерировать 16-битную сборку Real Mode x86, создавать двоичный файл, который удаляет всю вашу домашнюю папку, испускать код сборки Apollo 11 Guidance Computer или что-то еще. Это не ошибка. Это идеально соответствует стандарту.

3 голосов
/ 22 января 2020

Стандарт 2018 C определяет в пункте 3.4.3, параграфе 1 «неопределенное поведение» как:

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

Это довольно просто. Нет требований к стандарту. Таким образом, нет, стандарт не требует, чтобы поведение было "согласованным". Требования не предъявляются.

Кроме того, компиляторы, операционные системы и другие элементы, связанные с созданием и запуском программы, как правило, не предъявляют никаких требований «согласованности» в том смысле, о котором спрашивается в этом вопросе.

Приложение

Обратите внимание, что ответы, в которых говорится, что «все может случиться», неверны. Стандарт C гласит только, что it не предъявляет никаких требований, когда существует поведение, которое он считает «неопределенным». Он не отменяет другие требования и не имеет права их аннулировать. Любые спецификации компиляторов, операционных систем, архитектуры машин или законов о потребительских продуктах; или l aws физики; л aws логики; или другие ограничения по-прежнему применяются. Одна из ситуаций, в которой это имеет значение, - это просто ссылка на библиотеки программного обеспечения, не записанные в C: стандарт C не определяет, что происходит, но то, что происходит, все еще ограничено другими используемыми языками программирования и спецификациями библиотеки, а также компоновщик, операционная система и т. д.

0 голосов
/ 24 февраля 2020

Марко Бонелли привел причины, по которым такое поведение разрешено; Я хотел бы попытаться объяснить, почему это может быть практичным.

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

С учетом вашего кода можно ли ожидать от компилятора выполнения операции !y строго перед вызовом printf()? Я бы сказал, что если вы введете такие правила, не останется места для оптимизаций. Таким образом, компилятор должен свободно переписывать код как

void f (int x) {
  int y = x + x;
  int notY = !(x + x);
  if (x == (1 << 31))
    printf ("y: %d, !y: %d\n", y, notY);
}

. Теперь должно быть очевидно, что для любых входных данных, которые не вызывают переполнения, поведение будет идентичным. Однако в случае переполнения y и notY испытывают эффекты UB независимо, и оба могут стать 0, потому что почему бы и нет.

0 голосов
/ 23 января 2020

По какой-то причине возник миф о том, что авторы Стандарта использовали фразу «неопределенное поведение» для описания действий, которые ранние описания языка его изобретателем характеризовали как «машинно-зависимые», позволяли компиляторам выводить, что различные ничего бы не случилось. Хотя это правда, что Стандарт не требует, чтобы реализации обрабатывали такие действия осмысленно даже на платформах, где было бы естественное «машинно-зависимое» поведение, Стандарт также не требует, чтобы любая реализация была способна обрабатывать любые полезные программы осмысленно; реализация могла бы соответствовать, не имея возможности осмысленно обрабатывать что-либо, кроме одной придуманной и бесполезной программы. Это не противоречит намерению Стандарта: «Несмотря на то, что несовершенная реализация, вероятно, могла бы создать программу, которая удовлетворяет этому требованию, но все же может быть бесполезной, Комитет C89 считал, что такая изобретательность, вероятно, потребует больше работы, чем что-то полезное».

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

С практической точки зрения гарантирование чистой семантики циклического переноса стоит немного больше, чем если бы целочисленные вычисления вели себя так, как если бы они выполнялись на больших блоках. типы в неуказанные времена. Даже в отсутствие «оптимизации» даже прямое генерирование кода для выражения, подобного long1 = int1*int2+long2;, на многих платформах выиграло бы от возможности использовать результат команды умножения 16x16-> 32 или 32x32-> 64 напрямую, вместо того, чтобы иметь подписать-расширить нижнюю половину результата. Кроме того, разрешение компилятору оценивать x+1 как тип, больший, чем x, при его удобстве позволит ему заменить x+1 > y на x >= y - как правило, полезная и безопасная оптимизация.

Компиляторы, такие как g cc go дальше, однако. Хотя авторы Стандарта отметили, что при оценке чего-то вроде:

unsigned mul(unsigned short x, unsigned short y) { return x*y; }

решение Стандарта о продвижении x и y до подписано int не будет t отрицательно влияет на поведение по сравнению с использованием unsigned («Обе схемы дают одинаковый ответ в подавляющем большинстве случаев, и обе дают одинаковый эффективный результат в еще большем числе случаев в реализациях с арифметикой с двумя дополнениями c и тихим переходом на переполнение со знаком, то есть в большинстве текущих реализаций. ") g cc иногда будет использовать вышеупомянутую функцию для вывода в вызова, вызывая код, который x не может превышать INT_MAX/y. Я не видел доказательств того, что авторы Стандарта ожидали такого поведения, а тем более намеревались его поощрить. Хотя авторы g cc утверждают, что любой код, который вызвал бы переполнение в таких случаях, "не работает", я не думаю, что авторы стандарта согласятся с этим, поскольку при обсуждении соответствия они отмечают: "Цель состоит в том, чтобы дать у программиста есть реальный шанс создать мощные C программы, которые также являются очень переносимыми, и при этом они не кажутся совершенно полезными C программами, которые оказываются не переносимыми, а значит, и наречиями строго. "

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

...