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

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

#include  using namespace std; int main(){ int i = 0x10000000; int c = 0; do{ c++; i += i; cout << 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. (я еще не проверял ее).

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

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

 .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 

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

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

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

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

Это хорошо работает с icl и tcc кстати, но вы не можете полагаться на такие вещи …

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

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

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

 i += i; 

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

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

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

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

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

  • Почему SSE скалярный sqrt (x) медленнее, чем rsqrt (x) * x?
  • Векторизация с неуравновешенными буферами: использование VMASKMOVPS: создание маски из подсчета несоосности? Или не использовать этот insn вообще
  • Почему x86 маленький endian?
  • Является ли использование double быстрее, чем float?
  • Самый быстрый способ вычисления абсолютного значения с помощью SSE
  • Windows 64-разрядный реестр против 32-разрядного реестра
  • Что будет использоваться для обмена данными между streamами, выполняются на одном ядре с HT?
  • Возможно ли, чтобы процессор x86 соответствовал процессору ARM с точки зрения производительности на ватт?
  • Самая быстрая встроенная спин-блокировка
  • Использование LEA для значений, которые не являются адресами / указателями?
  • Что означают скобки в x86 asm?
  • Давайте будем гением компьютера.