Почему переполнение целого числа на 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;
}
так вот: переполнение целого числа со знаком является технически неопределенным поведением. Но 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. (я еще не проверил это.)
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 , 56(%r13)
je .L3
movzbl 67(%r13), %eax
.L4:
movsbl %al, %esi
movq %rbx, %rdi
addl , %r12d
call _ZNSo3putEc
movq %rax, %rdi
call _ZNSo5flushEv
cmpl , %r12d
jne .L5
6 ответов:
когда стандарт говорит, что это неопределенное поведение, значит. Все может случиться. "Все" включает в себя "обычно целые числа обертываются, но иногда происходят странные вещи".
Да, на процессорах x86 целые числа обычно заверните так, как вы ожидаете. это одно из тех исключений. компилятор предполагает, что вы не будете вызывать неопределенное поведение, и оптимизирует тест цикла. Если вы действительно хотите обернуть, пройдите
-fwrapvдоg++илиgccпри компиляции; это дает вам четко определенную семантику переполнения (twos-complement), но может повредить производительности.
все просто: неопределенное поведение - особенно с оптимизацией (
-O2) включен - значит что-нибудь может произойти.ваш код ведет себя так, как (вы) ожидается без
-O2переключатель.это работает довольно хорошо с icl и tcc, кстати, но вы не можете полагаться на такие вещи...
по данным этой, оптимизация gcc фактически использует переполнение целого числа со знаком. Это будет означать, что "ошибка" является дизайн.
важная вещь, чтобы отметить здесь заключается в том, что программы на C++ пишутся на C++ абстрактный автомат (который, как правило, эмулируемой с помощью инструкции оборудования). Тот факт, что вы компилируете для x86 составляет полностью не имеет отношения к тому, что это имеет неопределенное поведение.
компилятор может свободно использовать существование неопределенного поведения для улучшения своей оптимизации (путем удаления условного из цикла, как в этом примере). Нет никакой гарантии, или даже полезно, сопоставление между конструкциями уровня C++ и конструкциями машинного кода уровня x86 помимо требования, чтобы машинный код при выполнении давал результат, требуемый абстрактной машиной C++.
i += i;// переполнение не определено.
С -fwrapv это правильно. -fwrapv
пожалуйста, люди, неопределено поведение именно, что undefined. Это значит, что все может случиться. На практике (как и в этом случае) компилятор может считать это не вызываться и делать все, что угодно, если это может сделать код быстрее/меньше. Что происходит с кодом, который не должен выполняться, никто не догадывается. Это будет зависеть от окружающего кода (в зависимости от этого компилятор вполне может генерировать другой код), используемые переменные/константы, флаги компилятора ... О, и компилятор может обновляться и писать один и тот же код по-разному, или вы можете получить другой компилятор с другим представлением о генерации кода. Или просто получить другую машину, даже другая модель в той же архитектурной линии вполне может иметь свое собственное неопределенное поведение (посмотрите на неопределенные коды операций, некоторые предприимчивые программисты обнаружили, что на некоторых из этих ранних машин иногда делали полезные вещи...). Есть нет "компилятор дает определенное поведение на неопределенное поведение". Есть области, которые определены реализацией, и там вы должны быть в состоянии рассчитывать на то, что компилятор будет вести себя последовательно.
даже если компилятор должен был указать, что целочисленное переполнение должно рассматриваться как "некритическая" форма неопределенного поведения (как определено в приложении L), результат целочисленного переполнения должен, при отсутствии конкретного обещания платформы более конкретного поведения, как минимум рассматриваться как "частично неопределенное значение". В соответствии с такими правилами добавление 1073741824+1073741824 может произвольно рассматриваться как получение 2147483648 или -2147483648 или любого другого значения, которое было конгруэнтно 2147483648 mod 4294967296, а значения, полученные путем сложения, могут произвольно рассматриваться как любое значение, которое было конгруэнтно 0 mod 4294967296.
правила, позволяющие переполнению давать "частично-неопределенные значения", были бы достаточно четко определены, чтобы соблюдать букву и дух приложения L, но не помешали бы компилятору делать те же общеполезные выводы, которые были бы оправданы, если бы переполнения были неограниченным неопределенным поведением. Это помешало бы компилятору сделать некоторые фальшивые "оптимизации", основной эффект которых во многих случаях заключается в том, что программисты добавляют дополнительный беспорядок в код, единственной целью которого является предотвращение таких" оптимизаций"; будет ли это хорошо или нет, зависит от точки зрения.
Comments