Следующий код переходит в бесконечный цикл 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
gcc -S
.Ответы:
Когда в стандарте говорится, что это неопределенное поведение, это означает это . Все может случиться. «Все» включает «обычно целые числа, но иногда случаются странные вещи».
Да, на процессорах x86 целые числа обычно переносятся так, как вы ожидаете. Это одно из тех исключений. Компилятор предполагает, что вы не вызовете неопределенного поведения, и оптимизирует цикл тестирования. Если вам действительно нужен перенос, перейдите
-fwrapv
кg++
илиgcc
при компиляции; это дает вам четко определенную семантику переполнения (дополнение до двух), но может снизить производительность.источник
-fwrapv
. Спасибо за указание на это.Все просто: неопределенное поведение - особенно при включенной оптимизации (
-O2
) - означает, что все может случиться.Ваш код без
-O2
переключателя ведет себя так, как вы ожидали .Кстати, он отлично работает с icl и tcc, но на такие вещи полагаться нельзя ...
Согласно этому , оптимизация gcc фактически использует целочисленное переполнение со знаком. Это будет означать, что «ошибка» является преднамеренной.
источник
for (j = i; j < i + 10; ++j) ++k;
он будет просто установленk = 10
, поскольку это всегда будет истинным, если не происходит подписанного переполнения.Здесь важно отметить, что программы на C ++ написаны для абстрактной машины C ++ (которая обычно эмулируется с помощью аппаратных инструкций). Тот факт, что вы компилируете для x86, совершенно не имеет отношения к тому факту, что это имеет неопределенное поведение.
Компилятор может использовать наличие неопределенного поведения для улучшения своей оптимизации (удаляя условие из цикла, как в этом примере). Не существует гарантированного или даже полезного сопоставления между конструкциями уровня C ++ и конструкциями машинного кода уровня x86, кроме требования, чтобы машинный код при выполнении давал результат, требуемый абстрактной машиной C ++.
источник
// переполнение не определено.
С -fwrapv это правильно. -fwrapv
источник
Пожалуйста, люди, неопределенное поведение именно такое, undefined . Значит, всякое могло случиться. На практике (как в этом случае) компилятор может предположить, что он небыть вызванным, и делать все, что ему заблагорассудится, если это может сделать код быстрее / меньше. Остается только догадываться, что происходит с кодом, который не должен запускаться. Это будет зависеть от окружающего кода (в зависимости от этого компилятор вполне может сгенерировать другой код), используемых переменных / констант, флагов компилятора ... О, и компилятор мог бы обновиться и написать тот же код по-другому, или вы могли бы получить другой компилятор с другим взглядом на генерацию кода. Или просто возьмите другую машину, даже другая модель в той же архитектурной линейке вполне может иметь собственное неопределенное поведение (посмотрите неопределенные коды операций, некоторые предприимчивые программисты обнаружили, что на некоторых из этих ранних машин иногда действительно делали полезные вещи ...) , Не Существует нет"компилятор дает определенное поведение при неопределенном поведении". Есть области, которые определяются реализацией, и в них вы можете рассчитывать на согласованное поведение компилятора.
источник
Даже если компилятор должен указать, что целочисленное переполнение должно рассматриваться как «некритическая» форма неопределенного поведения (как определено в Приложении L), результат целочисленного переполнения должен, при отсутствии обещания конкретной платформы более конкретного поведения, быть как минимум рассматривается как «частично неопределенное значение». Согласно таким правилам, сложение 1073741824 + 1073741824 может произвольно рассматриваться как дающее 2147483648 или -2147483648 или любое другое значение, которое было бы конгруэнтно 2147483648 mod 4294967296, а значения, полученные сложением, могли произвольно рассматриваться как любое значение, которое было конгруэнтно 0 mod 4294967296.
Правила, позволяющие переполнению давать «частично неопределенные значения», должны быть достаточно четко определены, чтобы соответствовать букве и духу Приложения L, но не помешают компилятору делать такие же общеполезные выводы, которые были бы оправданы, если бы переполнение было неограниченным. Неопределенное поведение. Это помешало бы компилятору произвести фальшивую «оптимизацию», основной эффект которой во многих случаях состоит в том, чтобы требовать, чтобы программисты добавляли дополнительный беспорядок в код, единственной целью которого является предотвращение такой «оптимизации»; будет ли это хорошо или нет, зависит от точки зрения.
источник