Программа странно ведет себя в онлайн-среде IDE


82

Я наткнулся на следующую программу на C ++ ( источник ):

#include <iostream>
int main()
{
    for (int i = 0; i < 300; i++)
        std::cout << i << " " << i * 12345678 << std::endl;
}

Это выглядит как простая программа и дает правильный вывод на моем локальном компьютере, то есть что-то вроде:

0 0
1 12345678
2 24691356
...
297 -628300930
298 -615955252
299 -603609574

Но в онлайн-IDE, таких как codechef , он дает следующий результат:

0 0
1 12345678
2 24691356
...
4167 -95167326
4168 -82821648
4169 -7047597

Почему forцикл не заканчивается на 300? Также эта программа всегда завершается 4169. Почему, 4169а не какую-то другую ценность?


45
Вероятно, потому что знаковое целочисленное переполнение имеет неопределенное поведение.
eerorika

17
Я знаю, что подписанное переполнение - это UB, но это впечатляющий провал ...
Галик

45
Хороший урок не предполагать, что UB ограничен
MM

12
Мне любопытно, почему вы думаете, что первый результат - это «правильный результат».
Lightness Races in Orbit

5
@LightnessRacesinOrbit - ну, это напоминание является ценным!
davidbak

Ответы:


108

Я предполагаю, что онлайн-компиляторы используют GCC или совместимый компилятор. Конечно, любой другой компилятор также может выполнять такую ​​же оптимизацию, но документация GCC хорошо объясняет, что он делает:

-faggressive-loop-optimizations

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

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


Знаковое целочисленное переполнение имеет неопределенное поведение. Оптимизатор смог доказать, что любое значение iбольше 173 вызовет UB, и, поскольку он может предположить, что UB нет, он также может предположить, что iоно никогда не будет больше 173. Затем он может дополнительно доказать, что i < 300это всегда верно, и так что условие цикла можно оптимизировать.

Почему 4169, а не какое-то другое значение?

Эти сайты, вероятно, ограничивают количество выводимых строк (или символов, или байтов), которые они показывают, и имеют такое же ограничение.


3
Если компилятор может доказать, что будет UB для любого значения iбольше 173, то почему он не выдает предупреждение вместо того, чтобы выполнять бессмысленную оптимизацию?
Jabberwocky

7
@MichaelWalz Он испускает один.
HolyBlackCat

3
@MichaelWalz: Удаление избыточных логических тестов - не бессмысленная оптимизация; это очень полезно. Что было бы (в основном) бессмысленным, так это добавление дополнительного кода для отключения / отмены оптимизации при наличии неработающего исходного кода.

25
@MichaelWalz: компилятор не может надежно обнаруживать UB , как вы предлагаете (хотя иногда это может предупредить о вероятном наступлении, так как он на самом деле делает здесь). Вместо этого он может действовать из лучших побуждений, предполагая, что UB не будет . Это две, возможно, тонко, но на самом деле существенно разные вещи. Не то чтобы компилятор сказал «ага! UB! Теперь я могу включить такую-то оптимизацию » - оптимизация была всегда. Он делает такие вещи все время . Но пока ваша программа написана правильно, вы не заметите никаких изменений ее предполагаемой семантики.
Гонки

20
По аналогии, производитель входной двери вашего дома, возможно, решил, что она была бы более прочной, если бы в ней тактически поместили кусок металла где-то посередине. Вы никогда не заметите этого, если только вы не проделаете дыры в двери и не используете двери неправильно .
Гонки

40

«Неопределенное поведение не определено».(c)

Компилятор, используемый в codechef, похоже, использует следующую логику:

  1. Неопределенного поведения не может быть.
  2. i * 12345678переполняется и приводит к UB, если i > 173(при условии 32-битногоint ).
  3. Таким образом, iникогда не может превышать173 .
  4. Таким образом i < 300является лишним и может быть заменен на true.

Сам цикл кажется бесконечным. По-видимому, codechef просто останавливает программу через определенное время или обрезает вывод.


11
@ArpanMangal Я только что посчитал символы на выходе, и, похоже, это так 2^16. По-видимому, это совпадение, что оба обрезают вывод до 2^16символов.
HolyBlackCat

3
@ArpanMangal: носовые демоны, такие как 4169, сейчас тусуются на ideone и codechef. UB не определено, это может быть что угодно, в том числе носовые демоны. Если серьезно, попытка проанализировать UB - пустая трата времени, используйте это время, чтобы выяснить, как этого не допустить.
jmoreno

6
@jmoreno - это не пустая трата времени, если вас интересует дизайн компилятора
user253751

2
@jmoreno Хотя, на этот раз можно было это проанализировать. Понимание того, как именно UB ломает вещи, может быть полезно для заключения, в каких случаях UB приемлемо, если таковые имеются.
HolyBlackCat

4
@jmoreno Все, что делает Вселенная, правильно (по определению), так какой смысл изучать астрономию?
user253751

11

Вы вызываете поведение undefined, вероятно, на 174-й итерации внутри вашего forцикла, поскольку максимальное intзначение, вероятно, 2147483647еще не 174 * 123456789вычислено выражением, 2148147972которое является неопределенным поведением, поскольку нет целочисленного переполнения со знаком. Итак, вы наблюдаете эффекты UB, особенно с компилятором GCC с установленными флагами оптимизации в вашем случае. Скорее всего, компилятор предупредил бы вас об этом, выдав следующее предупреждение:

warning: iteration 174 invokes undefined behavior [-Waggressive-loop-optimizations]

Снимите -O2флажки оптимизации ( ), чтобы увидеть другие результаты.


4
Стоит отметить, что неопределенное поведение может иметь обратный эффект - поскольку UB произойдет на итерации 174, стандарт даже не требует, чтобы первые 173 итерации цикла выполнялись должным образом!

@Hurkyl Действительно. Несколько парадоксально, что UB заставляет всю программу, включая предыдущие операторы, отображать UB.
Рон

12
@Ron: Это не парадоксально. Либо программа имеет четко определенную семантику, либо нет. Период. Помните, что код C ++ - это не последовательность инструкций, которые нужно выполнить; это описание программы .
Гонки

@LightnessRacesinOrbit: программа может иметь семантику, которая частично не определена, но не полностью неопределена. Стандарт C пытается применить эту концепцию к тому, что он называет «ограниченным UB», хотя язык, который он использует, немного расплывчат. Предоставление компиляторам широкой, но не неограниченной свободы в обработке таких вещей, как целочисленное переполнение, не повлияет на многие полезные в других отношениях оптимизации, но сделает язык более подходящим для обработки данных, полученных из ненадежных источников.
supercat

@supercat: Действительно, неопределенное поведение не то же самое, что неопределенное поведение. Мы обсуждаем последнее
Гонки

7

Компилятор может предположить, что неопределенное поведение не произойдет, и, поскольку подписанное переполнение - это UB, он может предположить, что никогда i * 12345678 > INT_MAX, i <= INT_MAX / 12345678 < 300а следовательно, и удалить проверку i < 300.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.