Чому переповнення цілого числа на x86 з GCC викликає нескінченний цикл?


129

Наступний код переходить у нескінченний цикл на 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

10
Це було б набагато відповідальніше, якби ви включили згенерований код складання з gcc -S.
Грег Хьюгілл

Збірка напрочуд довга. Чи потрібно все-таки редагувати його?
Таємничий

Просто деталі, що стосуються вашого циклу, будь ласка.
Грег Х'югілл

12
-1. ви кажете, що це строго кажучи невизначена поведінка, і запитуєте, чи це не визначена поведінка. тож це для мене не справжнє питання.
Йоханнес Шауб - запалений

8
@ JohannesSchaub-litb Дякую за коментар. Можливо, погана формулювання з мого боку. Я постараюсь зробити все можливе, щоб заробити ваш скасування подання (і я редагую це питання відповідно). В основному, я знаю, що це UB. Але я також знаю, що GCC на x86 використовує цілі інструкції x86, які завершують переповнення. Тому я очікував, що він завершиться, незважаючи на те, що він є UB. Однак це не сталося, і це мене бентежило. Звідси питання.
Містичний

Відповіді:


178

Коли стандарт каже, що це невизначена поведінка, це означає . Все може статися. "Що-небудь" включає ", як правило, цілі числа обгортаються, але при нагоді трапляються дивні речі".

Так, на процесорах x86 цілі числа зазвичай відображають так, як ви очікуєте. Це одне з таких винятків. Компілятор припускає, що ви не будете викликати невизначене поведінку, і оптимізує тест циклу. Якщо ви дійсно хочете розгорнутись, перейдіть -fwrapvдо g++або gccпід час компілювання; це дає чітко визначену семантику переповнення (подвійні доповнення), але може зашкодити продуктивності.


24
Ух ти. Я не знав про це -fwrapv. Дякуємо, що вказали на це.
Містичний

1
Чи є варіант попередження, який намагається помітити випадкові нескінченні петлі?
Джефф Бурджес

5
Я знайшов згадувані тут оптимізації для оптимізації петлі: stackoverflow.com/questions/2982507/…
Джефф Бердджес

1
-1 "Так, на процесорах x86 цілі числа зазвичай обгортають так, як ви очікуєте." Це неправильно. але це тонко. наскільки я пам’ятаю, можна зробити так, щоб вони потрапили в пастку переповнення, але це не те, про що ми говоримо тут , і я ніколи не бачив цього робити. крім цього, і нехтуючи операціями x86 bcd (не дозволено представлення в C ++), цілі операційні знаки x86 завжди завершуються, тому що вони є двома доповненнями. ви помиляєтеся на помилкову оптимізацію g ++ (або вкрай непрактично і нісенітницю) для властивості x86 цілих ops.
Ура та хт. - Альф

5
@ Cheersandhth.-Alf, "на процесорах x86" я маю на увазі ", коли ви розробляєте для процесорів x86 за допомогою компілятора C". Мені справді потрібно це прописати? Очевидно, що всі мої розмови про компілятори та GCC не мають значення, якщо ви розробляєте асемблер, і в цьому випадку семантика для цілого переповнення дуже чітко визначена.
bdonlan

18

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

Ваш код поводиться так, як ви очікували без -O2перемикача.

До речі, це добре працює з icl та tcc, але ви не можете розраховувати на такі речі ...

Відповідно до цього , оптимізація gcc фактично використовує підписане ціле число. Це означало б, що "помилка" є задумом.


Це якось гадає, що компілятор обрав би нескінченний цикл усіх речей для невизначеної поведінки.
Зворотний

27
@ Inverse: Я не згоден. Якщо ви щось закодували з невизначеною поведінкою, моліться про нескінченну петлю. Це полегшує виявлення ...
Денніс

Я маю на увазі, якщо компілятор активно шукає UB, чому б не вставити виняток замість того, щоб намагатися гіпероптимізувати зламаний код?
Зворотний

15
@ Inverse: Компілятор активно не шукає невизначеної поведінки , він передбачає, що вона не відбувається. Це дозволяє компілятору оптимізувати код. Наприклад, замість обчислень for (j = i; j < i + 10; ++j) ++k;він буде просто встановлений k = 10, оскільки це завжди буде правдою, якщо не відбудеться переписаний підписаний підпис.
Денніс

@ Inverse Компілятор нічого не вибирав. Ви написали цикл у своєму коді. Компілятор цього не вигадав.
Гонки легкості на орбіті

13

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

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



3

Будь ласка, люди, невизначена поведінка саме це, невизначена . Це означає, що все може статися. На практиці (як у цьому випадку) компілятор вільний припустити, що цього не зробитьбути закликаним і робити все, що заманеться, якщо це може зробити код швидшим / меншим. Що станеться з кодом, який не повинен працювати, - це хтось здогадався. Це буде залежати від оточуючого коду (залежно від того, що компілятор міг би генерувати різний код), використовуваних змінних / констант, прапорів компілятора, ... О, і компілятор міг би оновлюватися і писати один і той же код по-іншому, або ви могли отримати інший компілятор з іншим поглядом на генерацію коду. Або просто придбайте іншу машину, навіть інша модель в тій же архітектурній лінії могла б мати власне невизначене поведінку (шукайте невизначені опкоди; деякі підприємливі програмісти дізналися, що на деяких із цих ранніх машин іноді робили корисні речі ...) . Чи не Існує немає"компілятор дає певну поведінку щодо невизначеної поведінки". Є області, визначені реалізацією, і там ви повинні мати можливість розраховувати на те, що компілятор поводиться послідовно.


1
Так, я дуже добре знаю, що таке невизначена поведінка. Але коли ви знаєте, як певні аспекти мови реалізовані для певного середовища, ви можете сподіватися побачити певні типи UB, а не інші. Я знаю, що GCC реалізує цілочисельну арифметику як цілу арифметику x86, яка завершується переповненням. Тож я припустив поведінку як таку. Я не очікував, що GCC зробить щось інше, як відповів bdonlan.
Містичний

7
Неправильно. Що відбувається, так це те, що GCC дозволено припускати, що ви не будете викликати невизначене поведінку, тому він просто випромінює код, як ніби цього не може статися. Якщо це дійсно станеться, то інструкція робити те , що ви просите з НЕ непередбачуваною поведінкою отримати виконуються, і результат незалежно від процесора робить. Тобто, на x86 є x86 речі. Якщо це інший процесор, він може зробити щось зовсім інше. Або компілятор міг би бути досить розумним, щоб зрозуміти, що ви закликаєте до невизначеної поведінки та почати nethack (так, деякі стародавні версії gcc зробили саме це).
vonbrand

4
Я вважаю, що ви неправильно прочитали мій коментар. Я сказав: "Чого я не очікував" - саме тому я задав питання в першу чергу. Я не очікував, що GCC підніме жодні хитрощі.
Містичний

1

Навіть якщо компілятор мав би вказати, що ціле переповнення повинно вважатися "некритичною" формою невизначеного поведінки (як визначено у Додатку L), результат переповнення цілого числа повинен бути відсутнім для конкретних платформних обіцянок більш конкретної поведінки. як мінімум розглядається як "частково невизначене значення". Згідно з такими правилами, додавання 1073741824 + 1073741824 можна довільно вважати таким, що дає 2147483648 або -2147483648 або будь-яке інше значення, що відповідає 2147483648 моду 4294967296, а значення, отримані доповненнями, можуть довільно вважатись будь-яким значенням, що відповідає 0 моду 4294967296.

Правила, що дозволяють переповненню отримати "частково невизначені значення", були б досить чітко визначені, щоб дотримуватися букви та духу Додатку L, але не заважали компілятору робити ті ж загальнокорисні умовиводи, як це було б обґрунтовано, якби переливи не обмежувались Невизначена поведінка. Це не дало б компілятору зробити якісь фальшиві "оптимізації", основний ефект яких у багатьох випадках полягає в тому, щоб вимагати від програмістів додавати зайвого безладу коду, єдиною метою якого є запобігання таких "оптимізацій"; добре це чи ні, залежить від точки зору.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.