Компилируя это:
#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)
c++
gcc
undefined-behavior
zerkms
источник
источник
O2
, иO3
флагом, а не сO0
илиO1
Ответы:
Целочисленное переполнение со знаком (строго говоря, не существует такого понятия, как переполнение без знака), что означает неопределенное поведение . А это значит, что все может случиться, и обсуждать, почему это происходит по правилам C ++, не имеет смысла.
C ++ 11 черновик N3337: §5.4: 1
Ваш код, скомпилированный с
g++ -O3
предупреждением о выбросах (даже без-Wall
)Единственный способ проанализировать, что делает программа, это прочитать сгенерированный код сборки.
Вот полный список сборки:
Я едва могу даже прочитать сборку, но даже я вижу
addl $1000000000, %edi
линию. Полученный код больше похож наЭтот комментарий @TC:
дал мне идею сравнить код сборки кода OP с кодом сборки следующего кода без неопределенного поведения.
И действительно, правильный код имеет условие завершения.
Смирившись с этим, вы написали глючный код, и вам должно быть плохо. Нести последствия.
... или, наоборот, правильно использовать улучшенные средства диагностики и отладки - вот для чего они нужны:
включить все предупреждения
-Wall
опция gcc, которая включает все полезные предупреждения без ложных срабатываний Это минимум, который вы всегда должны использовать.-Wall
как могут предупреждать о ложных срабатыванияхиспользовать отладочные флаги для отладки
-ftrapv
перехватывает программу при переполнении,-fcatch-undefined-behavior
ловит много случаев неопределенного поведения (примечание:"a lot of" != "all of them"
)Используйте GCC
-fwrapv
1 - это правило не применяется к "переполнению целых чисел без знака", так как в п. 3.9.1.4 сказано, что
и, например, результат
UINT_MAX + 1
математически определяется - по правилам арифметики по модулю 2 nисточник
i
влияет на себя? Вообще неопределенное поведение не имеет такого рода странных побочных эффектов, в конце концов,i*100000000
должно быть значениеi
любым значением больше 2 имеет неопределенное поведение -> (2) мы можем предположить, чтоi <= 2
в целях оптимизации -> (3) условие цикла всегда выполняется -> (4) ) он оптимизирован в бесконечный цикл.i
на 1e9 каждую итерацию (и соответственно изменяет условие цикла). Это вполне допустимая оптимизация в соответствии с правилом «как будто», так как эта программа не могла наблюдать разницу, если бы она работала хорошо. Увы, это не так, и оптимизация «протекает».Короткий ответ, в
gcc
частности, задокументировал эту проблему, мы можем видеть, что в примечаниях к выпуску gcc 4.8, который говорит ( выделение мое в будущем ):и действительно, если мы используем
-fno-aggressive-loop-optimizations
бесконечный цикл, поведение должно прекратиться, и это происходит во всех случаях, которые я проверял.Длинные начинает ответ с зная , что знаковое целое переполнение не определено поведение, глядя на проект C ++ стандартный раздел
5
Выражения пункт 4 , который гласит:Мы знаем, что стандарт говорит, что неопределенное поведение непредсказуемо из примечания, которое идет с определением, которое говорит:
Но что в мире может сделать
gcc
оптимизатор, чтобы превратить это в бесконечный цикл? Это звучит совершенно странно. Но, к счастью,gcc
дает нам ключ к разгадке этого предупреждения:Подсказка в том
Waggressive-loop-optimizations
, что это значит? К счастью для нас, это не первый раз, когда эта оптимизация ломает код таким образом, и нам повезло, потому что Джон Регер задокументировал случай в статье GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks, в которой показан следующий код:в статье говорится:
и позже говорит:
Поэтому то, что компилятор должен делать в некоторых случаях, предполагает, что целочисленное переполнение со знаком является неопределенным поведением, тогда оно
i
всегда должно быть меньше,4
и, таким образом, мы имеем бесконечный цикл.Он объясняет, что это очень похоже на печально известное удаление проверки нулевого указателя ядра Linux, когда мы видим этот код:
gcc
Предполагается, что, поскольку онs
был определен вs->f;
и так как разыменование нулевого указателя является неопределенным поведением, то онs
не должен быть нулевым, и поэтому оптимизируетif (!s)
проверку на следующей строке.Урок здесь заключается в том, что современные оптимизаторы очень агрессивны в отношении использования неопределенного поведения и, скорее всего, станут более агрессивными. Ясно, что всего несколько примеров показывают, что оптимизатор делает вещи, которые кажутся программисту совершенно необоснованными, но в ретроспективе с точки зрения оптимизаторов имеет смысл.
источник
tl; dr Код генерирует тест, в котором целое число + положительное целое число == отрицательное целое число . Обычно оптимизатор не оптимизирует это, но в конкретном случае
std::endl
следующего использования компилятор оптимизирует этот тест. Я еще не понял, что особенного вendl
этом.Из кода сборки на уровне -O1 и выше видно, что gcc рефакторинг цикла:
Самое большое значение, которое работает правильно, это
715827882
, например, floor (INT_MAX/3
). Сборочный фрагмент по адресу-O1
:Обратите внимание, что
-1431655768
это4 * 715827882
в дополнении 2.Удар
-O2
оптимизирует это к следующему:Таким образом, оптимизация была сделана просто потому, что
addl
она поднялась выше.Если мы перекомпилируем
715827883
вместо этого, тогда версия -O1 идентична, кроме измененного номера и тестового значения. Однако -O2 вносит изменения:Там , где было
cmpl $-1431655764, %esi
на-O1
, что линия была удалена для-O2
. Оптимизатор должен решил , что добавление715827883
к%esi
никогда не сравнится-1431655764
.Это довольно загадочно. Добавим , что в
INT_MIN+1
это генерировать ожидаемый результат, поэтому оптимизатор должен решил , что%esi
никогда не может быть ,INT_MIN+1
и я не знаю , почему это было бы решить , что.В рабочем примере кажется одинаково верным заключить, что добавление
715827882
к числу не может быть равнымINT_MIN + 715827882 - 2
! (это возможно только в том случае, если на самом деле имеет место перенос), но в этом примере он не оптимизирует строку.Код, который я использовал:
Если
std::endl(std::cout)
удалить, то оптимизация больше не происходит. Фактически, его заменаstd::cout.put('\n'); std::flush(std::cout);
также приводит к тому, что оптимизация не происходит, даже еслиstd::endl
она встроена.Кажется, что встраивание элемента
std::endl
влияет на более раннюю часть структуры цикла (что я не совсем понимаю, что она делает, но я опубликую ее здесь на случай, если кто-то другой):С оригинальным кодом и
-O2
:С mymanual встраивание в
std::endl
,-O2
:Одно из различий между этими двумя заключается в том, что они
%esi
используются в оригинале и%ebx
во второй версии; Есть ли разница в семантике, определенной между%esi
и%ebx
в целом? (Я не знаю много о сборке x86).источник
Другой пример этой ошибки, о которой сообщается в gcc, - это когда у вас есть цикл, который выполняется для постоянного числа итераций, но вы используете переменную counter в качестве индекса в массиве, который содержит меньше этого числа элементов, например:
Компилятор может определить, что этот цикл будет пытаться получить доступ к памяти вне массива «a». Компилятор жалуется на это с довольно загадочным сообщением:
источник
Кажется, что целочисленное переполнение происходит в 4-й итерации (для
i = 3
).signed
Целочисленное переполнение вызывает неопределенное поведение . В этом случае ничего нельзя предсказать. Цикл может повторяться только4
раз, или он может идти до бесконечности или что-то еще!Результат может варьироваться от компилятора к компилятору или даже для разных версий одного и того же компилятора.
C11: 1.3.24 неопределенное поведение:
источник