Почему целочисленное переполнение на x86 с GCC вызывает бесконечный цикл?

129

Следующий код переходит в бесконечный цикл GCC:

#include <iostream>
using namespace std;

int main(){
    int i = 0x10000000;

    int c = 0;
    do{
        c++;
        i += i;
        cout << i << endl;
    }while (i > 0);

    cout << c << endl;
    return 0;
}

Итак, дело в следующем: подписанное целочисленное переполнение технически неопределенное поведение. Но GCC на x86 реализует целочисленную арифметику с использованием целочисленных инструкций x86, которые переносятся при переполнении.

Поэтому я ожидал, что это произойдет при переполнении - несмотря на то, что это неопределенное поведение. Но это явно не так. Так что я пропустил?

Я скомпилировал это, используя:

~/Desktop$ g++ main.cpp -O2

Вывод GCC:

~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0

... (infinite loop)

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

Правильный вывод:

~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3

Вот еще несколько вариантов:

i *= 2;   //  Also fails and goes into infinite loop.
i <<= 1;  //  This seems okay. It does not enter infinite loop.

Вот вся соответствующая информация о версии:

~/Desktop$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ..

...

Thread model: posix
gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4) 
~/Desktop$ 

Итак, вопрос: это ошибка в GCC? Или я неправильно понял, как GCC обрабатывает целочисленную арифметику?

* Я также помечаю этот C, потому что предполагаю, что эта ошибка будет воспроизведена в C. (я еще не проверял это).

РЕДАКТИРОВАТЬ:

Вот сборка петли: (если я правильно распознал)

.L5:
addl    %ebp, %ebp
movl    $_ZSt4cout, %edi
movl    %ebp, %esi
.cfi_offset 3, -40
call    _ZNSolsEi
movq    %rax, %rbx
movq    (%rax), %rax
movq    -24(%rax), %rax
movq    240(%rbx,%rax), %r13
testq   %r13, %r13
je  .L10
cmpb    $0, 56(%r13)
je  .L3
movzbl  67(%r13), %eax
.L4:
movsbl  %al, %esi
movq    %rbx, %rdi
addl    $1, %r12d
call    _ZNSo3putEc
movq    %rax, %rdi
call    _ZNSo5flushEv
cmpl    $3, %r12d
jne .L5
Mysticial
источник
10
Это было бы гораздо более ответственно, если бы вы включили сгенерированный код сборки из gcc -S.
Грег Хьюгилл
Сборка на удивление долгая. Стоит ли мне редактировать его?
Mysticial
Пожалуйста, только те части, которые относятся к вашему циклу.
Грег Хьюгилл
12
-1. вы говорите, что это, строго говоря, неопределенное поведение, и спрашиваете, не является ли это неопределенным поведением. так что это не настоящий вопрос для меня.
Йоханнес Шауб - litb
8
@ JohannesSchaub-litb Спасибо за комментарий. Наверное, с моей стороны плохая формулировка. Я сделаю все возможное, чтобы внести ясность, чтобы заслужить ваш голос без ответа (и я соответствующим образом отредактирую вопрос). В принципе, я знаю, что это УБ. Но я также знаю, что GCC на x86 использует целочисленные инструкции x86, которые переносятся при переполнении. Поэтому я ожидал, что он завершится, несмотря на то, что это UB. Однако этого не произошло, и это меня смутило. Отсюда вопрос.
Mysticial

Ответы:

178

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

Да, на процессорах x86 целые числа обычно переносятся так, как вы ожидаете. Это одно из тех исключений. Компилятор предполагает, что вы не вызовете неопределенного поведения, и оптимизирует цикл тестирования. Если вам действительно нужен перенос, перейдите -fwrapvк g++или gccпри компиляции; это дает вам четко определенную семантику переполнения (дополнение до двух), но может снизить производительность.

bdonlan
источник
24
Ух ты. Я не знал об этом -fwrapv. Спасибо за указание на это.
Mysticial
1
Есть ли опция предупреждения, которая пытается заметить случайные бесконечные циклы?
Джефф Берджес
5
Я обнаружил, что -Wunsafe-loop-optimizations, упомянутые здесь: stackoverflow.com/questions/2982507/…
Джефф Берджес,
1
-1 «Да, на процессорах x86 целые числа обычно переносятся так, как вы ожидаете». это неверно. но это тонко. насколько я помню, их можно поймать в ловушку при переполнении, но мы говорим не об этом , и я никогда не видел, чтобы это делалось. кроме этого, и игнорируя операции x86 bcd (недопустимое представление в C ++) целочисленные операции x86 всегда переносятся, потому что они дополняют два. вы ошибочно принимаете ошибочную (или крайне непрактичную и бессмысленную) оптимизацию g ++ за свойство целочисленных операций x86.
Приветствия и hth. - Alf
5
@ Cheersandhth.-Alf, «на процессорах x86» я имею в виду «когда вы разрабатываете для процессоров x86 с использованием компилятора C». Мне действительно нужно это разъяснять? Очевидно, все мои разговоры о компиляторах и GCC неуместны, если вы разрабатываете на ассемблере, и в этом случае семантика для целочисленного переполнения действительно очень хорошо определена.
bdonlan
18

Все просто: неопределенное поведение - особенно при включенной оптимизации ( -O2) - означает, что все может случиться.

Ваш код без -O2переключателя ведет себя так, как вы ожидали .

Кстати, он отлично работает с icl и tcc, но на такие вещи полагаться нельзя ...

Согласно этому , оптимизация gcc фактически использует целочисленное переполнение со знаком. Это будет означать, что «ошибка» является преднамеренной.

Деннис
источник
Отстойно, что компилятор выбрал бы бесконечный цикл из всех вещей для неопределенного поведения.
Inverse
27
@Inverse: я не согласен. Если вы закодировали что-то с неопределенным поведением, молитесь о бесконечном цикле. Облегчает обнаружение ...
Деннис
Я имею в виду, что если компилятор активно ищет UB, почему бы не вставить исключение вместо попытки гипероптимизировать сломанный код?
Inverse
15
@Inverse: компилятор активно не ищет неопределенного поведения , он предполагает, что этого не происходит. Это позволяет компилятору оптимизировать код. Например, вместо вычисления for (j = i; j < i + 10; ++j) ++k;он будет просто установлен k = 10, поскольку это всегда будет истинным, если не происходит подписанного переполнения.
Деннис
@Inverse Компилятор ничего не «выбирал». Вы написали цикл в своем коде. Компилятор это не изобрел.
Гонки за легкостью на орбите
13

Здесь важно отметить, что программы на C ++ написаны для абстрактной машины C ++ (которая обычно эмулируется с помощью аппаратных инструкций). Тот факт, что вы компилируете для x86, совершенно не имеет отношения к тому факту, что это имеет неопределенное поведение.

Компилятор может использовать наличие неопределенного поведения для улучшения своей оптимизации (удаляя условие из цикла, как в этом примере). Не существует гарантированного или даже полезного сопоставления между конструкциями уровня C ++ и конструкциями машинного кода уровня x86, кроме требования, чтобы машинный код при выполнении давал результат, требуемый абстрактной машиной C ++.

Mankarse
источник
5
i += i;

// переполнение не определено.

С -fwrapv это правильно. -fwrapv

lostyzd
источник
3

Пожалуйста, люди, неопределенное поведение именно такое, undefined . Значит, всякое могло случиться. На практике (как в этом случае) компилятор может предположить, что он небыть вызванным, и делать все, что ему заблагорассудится, если это может сделать код быстрее / меньше. Остается только догадываться, что происходит с кодом, который не должен запускаться. Это будет зависеть от окружающего кода (в зависимости от этого компилятор вполне может сгенерировать другой код), используемых переменных / констант, флагов компилятора ... О, и компилятор мог бы обновиться и написать тот же код по-другому, или вы могли бы получить другой компилятор с другим взглядом на генерацию кода. Или просто возьмите другую машину, даже другая модель в той же архитектурной линейке вполне может иметь собственное неопределенное поведение (посмотрите неопределенные коды операций, некоторые предприимчивые программисты обнаружили, что на некоторых из этих ранних машин иногда действительно делали полезные вещи ...) , Не Существует нет"компилятор дает определенное поведение при неопределенном поведении". Есть области, которые определяются реализацией, и в них вы можете рассчитывать на согласованное поведение компилятора.

vonbrand
источник
1
Да, я очень хорошо знаю, что такое неопределенное поведение. Но когда вы знаете, как определенные аспекты языка реализованы для конкретной среды, вы можете ожидать увидеть одни типы UB, а не другие. Я знаю, что GCC реализует целочисленную арифметику как целочисленную арифметику x86, которая оборачивается при переполнении. Итак, я принял поведение как таковое. Чего я не ожидал, так это того, что GCC сделает что-то еще, как ответил bdonlan.
Mysticial
7
Неправильно. Что происходит, так это то, что GCC разрешено предполагать, что вы не будете вызывать неопределенное поведение, поэтому он просто выдает код, как будто этого не может быть. Если это все же произойдет, будут выполнены инструкции по выполнению того, что вы просите, без неопределенного поведения, и результатом будет то, что делает ЦП. Т.е. на x86 есть х86. Если это другой процессор, он может делать что-то совершенно другое. Или компилятор может быть достаточно умен, чтобы понять, что вы вызываете неопределенное поведение, и запустить nethack (да, некоторые древние версии gcc сделали именно это).
vonbrand
4
Я полагаю, вы неправильно прочитали мой комментарий. Я сказал: «Чего я не ожидал» - именно поэтому я задал этот вопрос в первую очередь. Я не ожидал, что GCC проведет какие-либо уловки.
Mysticial
1

Даже если компилятор должен указать, что целочисленное переполнение должно рассматриваться как «некритическая» форма неопределенного поведения (как определено в Приложении L), результат целочисленного переполнения должен, при отсутствии обещания конкретной платформы более конкретного поведения, быть как минимум рассматривается как «частично неопределенное значение». Согласно таким правилам, сложение 1073741824 + 1073741824 может произвольно рассматриваться как дающее 2147483648 или -2147483648 или любое другое значение, которое было бы конгруэнтно 2147483648 mod 4294967296, а значения, полученные сложением, могли произвольно рассматриваться как любое значение, которое было конгруэнтно 0 mod 4294967296.

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

Supercat
источник