Почему целочисленное переполнение на 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, которые переносятся на переполнение.
Поэтому я ожидал, что он перевернется на переполнение – несмотря на то, что это неопределенное поведение. Но это явно не так. Так что я пропустил?
- x86 Инструкция MUL от VS 2008/2010
- Какова цель регистра указателя кадров EBP?
- Как точно работает инструкция x86 LOOP?
- Что такое рамка стека в сборке?
- Assembly x86 Date to Number - разбиение строки на более мелкие разделы
Я скомпилировал это, используя:
~/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
- Безопасно ли читать конец конца буфера на одной странице на x86 и x64?
- Самый быстрый способ сделать горизонтальную векторную сумму float на x86
- 8086 на DOSBox: ошибка с инструкцией idiv?
- Почему этот код SSE в 6 раз медленнее без VZEROUPPER на Skylake?
- Ссылка на содержимое ячейки памяти. (режимы адресации x86)
- Что происходит, когда запускается компьютерная программа?
- SIMD, подписанный с неподписанным умножением для 64-разрядных * 64-бит до 128 бит
- Каковы наилучшие последовательности команд для генерации векторных констант «на лету»?
Когда стандарт говорит, что это неопределенное поведение, это означает это . Все может случиться. «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 таких «оптимизаций»; будет ли это хорошо или нет, зависит от вашей точки зрения.