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

Следующий код переходит в бесконечный цикл на 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;
}

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

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

Я скомпилировал это с помощью:

~/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. (я еще не проверял ее.)

EDIT:

Здесь сборка цикла: (если я правильно ее распознал)

.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

Ответ 1

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

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

Ответ 2

Это просто: поведение Undefined - особенно при оптимизации (-O2) - означает что-либо.

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

Он отлично работает с icl и tcc кстати, но вы не можете полагаться на такие вещи...

В соответствии с этим gcc-оптимизация фактически использует подписанное целочисленное переполнение. Это означало бы, что "ошибка" по дизайну.

Ответ 3

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

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

Ответ 4

i += i;

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

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

Ответ 5

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

Ответ 6

Даже если компилятор должен был указать, что целочисленное переполнение должно рассматриваться как "некритическая" форма Undefined Behavior (как определено в Приложении L), результат переполнения целочисленного значения должен, более конкретное поведение, как минимум, рассматривается как "частично неопределенное значение". В соответствии с такими правилами добавление 1073741824 + 1073741824 можно условно считать доходностью 2147483648 или -2147483648 или любым другим значением, которое соответствовало 2147483648 моду 4294967296, а значения, полученные дополнениями, можно было бы условно рассматривать как любое значение, которое соответствовало 0 mod 4294967296.

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