[C++] Почему этот цикл создает «предупреждение: итерация 3u вызывает неопределенное поведение» и выводит более 4 строк?


Answers

Короткий ответ, gcc специально задокументировал эту проблему, мы можем видеть, что в примечаниях к выпуску gcc 4.8, в которых говорится ( акцент мой идет вперед ):

GCC теперь использует более агрессивный анализ, чтобы получить верхнюю границу для количества итераций циклов, используя ограничения, установленные языковыми стандартами . Это может привести к тому, что несоответствующие программы перестанут работать должным образом, например SPEC CPU 2006 464.h264ref и 416.gamess. Для отключения этого агрессивного анализа была добавлена ​​новая опция -fno-aggressive-loop-optimizations. Известно, что в некоторых циклах, которые знают постоянное число итераций, но неопределенное поведение происходит в цикле до достижения или во время последней итерации, GCC будет предупреждать о неопределенном поведении в цикле, а не выводить нижнюю верхнюю границу числа итераций для цикла. Предупреждение можно отключить с помощью -Wno-агрессивных циклов-оптимизации.

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

Длительный ответ начинается с того, что известно, что знаковое целочисленное переполнение является неопределенным поведением, рассматривая проект 4 параграфа 5 C ++, который гласит:

Если во время оценки выражения результат не определяется математически или нет в диапазоне представляемых значений для его типа, поведение не определено . [Примечание: большинство существующих реализаций C ++ игнорируют целые переполнения. Обработка деления на ноль, формирование остатка с использованием делителя нуля, и все исключения с плавающей запятой различаются между машинами и обычно регулируются библиотечной функцией. -end note

Мы знаем, что стандарт говорит, что неопределенное поведение непредсказуемо из примечания, которое приходит с определением, которое гласит:

[Примечание. Неопределенное поведение можно ожидать, если в этом международном стандарте отсутствует явное определение поведения или когда программа использует ошибочную конструкцию или ошибочные данные. Допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами , ведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или исполнения (с выдачей диагностического сообщения). Многие ошибочные программные конструкции не порождают неопределенное поведение; они должны быть диагностированы. -end note]

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

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^

Ключ - Waggressive-loop-optimizations , что это значит? К счастью для нас, это не первый раз, когда эта оптимизация нарушает код таким образом, и нам повезло, потому что Джон Реджер задокументировал случай в статье GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks, которая показывает следующий код:

int d[16];

int SATD (void)
{
  int satd = 0, dd, k;
  for (dd=d[k=0]; k<16; dd=d[++k]) {
    satd += (dd < 0 ? -dd : dd);
  }
  return satd;
}

в статье говорится:

Неопределенное поведение обращается к d [16] непосредственно перед выходом из цикла. В C99 законно создавать указатель на элемент на одну позицию за конец массива, но этот указатель не должен быть разыменован.

а затем говорит:

В деталях, вот что происходит. AC-компилятор, увидев d [++ k], разрешено считать, что приращение значения k находится в пределах массива, так как в противном случае происходит неопределенное поведение. Для кода здесь GCC может сделать вывод, что k находится в диапазоне 0..15. Немного позже, когда GCC видит k <16, он говорит себе: «Ага - это выражение всегда истинно, поэтому у нас есть бесконечный цикл» . Ситуация здесь, когда компилятор использует предположение о четкости, чтобы сделать вывод полезный факт потока данных,

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

Он объясняет, что это очень похоже на отвратительное удаление указателя нулевого указателя ядра Linux, где при просмотре этого кода:

struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;

gcc предполагал, что поскольку s было отсрочено в s->f; и поскольку разыменование нулевого указателя является неопределенным поведением, то s не должно быть нулевым и, следовательно, оптимизирует проверку if (!s) на следующей строке.

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

Question

Компиляция:

#include <iostream>

int main()
{
    for (int i = 0; i < 4; ++i)
        std::cout << i*1000000000 << std::endl;
}

и gcc выдает следующее предупреждение:

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^

Я понимаю, что существует целочисленное переполнение со знаком.

То, что я не могу получить, - это то, почему значение i нарушено этой операцией переполнения?

Я прочитал ответы на вопрос Почему целое переполнение на x86 с GCC вызывает бесконечный цикл? , но я все еще не понимаю, почему это происходит - я понимаю, что «неопределенное» означает «что-то может случиться», но в чем причина этого конкретного поведения ?

В сети: http://ideone.com/dMrRKR

Компилятор: gcc (4.8)




То, что я не могу получить, - это то, почему значение i нарушено этой операцией переполнения?

Кажется, что целочисленное переполнение происходит на 4-й итерации (для i = 3 ). signed целочисленное переполнение вызывает неопределенное поведение . В этом случае ничего нельзя предсказать. Цикл может повторяться только 4 раза или он может перейти в бесконечное или что-то еще!
Результат может отличаться от компилятора для компилятора или даже для разных версий одного и того же компилятора.

C11: 1.3.24 неопределенное поведение:

поведение, для которого настоящий международный стандарт не предъявляет никаких требований
[Примечание. Неопределенное поведение можно ожидать, если в этом международном стандарте отсутствует явное определение поведения или когда программа использует ошибочную конструкцию или ошибочные данные. Допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами, ведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или исполнения (с выдачей диагностического сообщения) . Многие ошибочные программные конструкции не порождают неопределенное поведение; они должны быть диагностированы. -end note]