Почему целочисленное переполнение в x86 с GCC вызывает бесконечный цикл? - PullRequest
125 голосов
/ 07 октября 2011

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

Ответы [ 6 ]

170 голосов
/ 07 октября 2011

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

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

18 голосов
/ 07 октября 2011

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

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

Между прочим, с icl и tcc он работает довольно хорошо, но на такие вещи нельзя полагаться ...

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

11 голосов
/ 07 октября 2011

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

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

4 голосов
/ 07 октября 2011
i += i;

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

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

3 голосов
/ 20 января 2013

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

1 голос
/ 13 мая 2015

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

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

...