(A + B + C) ≠ (A + C + B) і упорядкування компілятора


108

Додавання двох 32-бітних цілих чисел може призвести до переповнення цілого числа:

uint64_t u64_z = u32_x + u32_y;

Цього переповнення можна уникнути, якщо одне із 32-розрядних цілих чисел спочатку переведено на кастинг або додане до 64-бітного цілого числа.

uint64_t u64_z = u32_x + u64_a + u32_y;

Однак якщо компілятор вирішить змінити порядок додавання:

uint64_t u64_z = u32_x + u32_y + u64_a;

переповнення цілого числа все-таки може відбутися.

Чи дозволяється компіляторам проводити таке переупорядкування чи ми можемо їм довіряти помічати результат невідповідності та зберігати порядок виразів таким, яким є?


15
Насправді ви не показуєте цілочисельне переповнення, оскільки, здається, вам додані uint32_tзначення - які не переповнюються, вони завершуються. Це не різні форми поведінки.
Мартін Боннер підтримує Моніку

5
Дивіться розділ 1.9 стандартів c ++, він безпосередньо відповідає на ваше запитання (є навіть приклад, який майже точно такий же, як і ваш).
Холт

3
@Tal: Як уже говорили інші: не існує переповнення цілих чисел. Непідписані визначені для обгортання, для підписаних це невизначена поведінка, тому будь-яка реалізація буде робити, включаючи носові демони.
занадто чесний для цього сайту

5
@Tal: Дурниці! Як я вже писав: стандарт дуже чіткий і вимагає обгортання, а не насичення (це було б можливо при підписанні, оскільки це UB як стандарт)
занадто чесний для цього сайту

15
@rustyx: Якщо ви дзвоните це обгортання або переливу, залишки точки , що ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0призводить 0, в той час як (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)результати в 0x100000000, і ці два значення не рівні. Тому важливо, чи може компілятор застосувати це перетворення чи ні. Але так, стандарт використовує лише слово "overflow" для підписаних цілих чисел, а не без підпису.
Стів Джессоп

Відповіді:


84

Якщо оптимізатор робить таке переупорядкування, він все ще пов'язаний зі специфікацією C, то таке переупорядкування стане:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

Обгрунтування:

Почнемо з

uint64_t u64_z = u32_x + u64_a + u32_y;

Додавання виконується зліва направо.

У цілих правилах просування зазначено, що в першому доповненні в оригінальному виразі u32_xслід переходити до uint64_t. У другому додатку u32_yтакож буде просуватися до uint64_t.

Таким чином, для того, щоб відповідати специфікації C, будь-який оптимізатор повинен просувати u32_xта u32_y64-бітні непідписані значення. Це еквівалентно доданню акторського складу. (Фактична оптимізація не проводиться на рівні С, але я використовую позначення C, оскільки це позначення, які ми розуміємо.)


Хіба це не ліво-асоціативний, так (u32_x + u32_t) + u64_a?
Марно

12
@Useless: Клас все кинув на 64 біт. Тепер замовлення взагалі не має ніякої різниці. Компілятору не потрібно дотримуватися асоціативності, він просто повинен дати точно такий же результат, як і в цьому випадку.
gnasher729

2
Здається, можна припустити, що код ОП буде оцінюватися так, що не відповідає дійсності.
Марно

@Klas - подбайте, щоб пояснити, чому це так, і як саме ви прийшли до вашого зразка коду?
іржа

1
@rustyx Це було потрібно пояснення. Дякуємо, що підштовхнули мене додати його.
Klas Lindbäck

28

Повторне замовлення компілятора дозволено лише за правилом, як ніби . Тобто, якщо переупорядкування завжди дасть той самий результат, що і вказане замовлення, тоді це дозволено. Інакше (як у вашому прикладі), ні.

Наприклад, з урахуванням наступного виразу

i32big1 - i32big2 + i32small

яке було ретельно побудовано для вирахування двох значень, які, як відомо, є великими, але схожими, і лише потім додає інше мале значення (таким чином, уникаючи будь-якого переповнення), компілятор може обрати перестановку на:

(i32small - i32big2) + i32big1

і покладатися на те, що цільова платформа використовує арифметику з двома доповненнями з обертанням для запобігання проблем. (Таке переупорядкування може бути розумним, якщо компілятор натиснений на регістри, і, мабуть, вже є i32smallв регістрі).


У прикладі ОП використовуються неподписані типи. i32big1 - i32big2 + i32smallМає на увазі підписані типи. Додаткові побоювання вступають у гру.
chux

@chux Абсолютно. Суть, яку я намагався зробити, полягає в тому, що хоча я не міг писати (i32small-i32big2) + i32big1, (тому що це може спричинити UB), компілятор може переставити його так ефективно, оскільки компілятор може бути впевнений, що поведінка буде правильною.
Мартін Боннер підтримує Моніку

3
@chux: Додаткові занепокоєння, такі як UB, не грають, тому що ми говоримо про компілятор, який упорядковує за правилом як-якщо. Конкретний компілятор може скористатися тим, що знає власну поведінку переповнення.
MSalters

16

У C, C ++ та Objective-C існує правило "як би": компілятор може робити все, що завгодно, доки жодна відповідна програма не може визначити різницю.

У цих мовах a + b + c визначається таким же, як (a + b) + c. Якщо ви можете сказати різницю між цим і, наприклад, + (b + c), компілятор не може змінити порядок. Якщо ви не можете сказати різницю, тоді компілятор вільний змінити порядок, але це добре, тому що ви не можете сказати різницю.

У вашому прикладі, якщо b = 64 біт, a і c 32 біт, компілятору буде дозволено оцінювати (b + a) + c або навіть (b + c) + a, тому що ви не могли сказати різницю, але не (a + c) + b, тому що ви можете сказати різницю.

Іншими словами, компілятору заборонено робити що-небудь, що змушує ваш код вести себе відмінним від того, що повинен. Вам нема потреби код , який ви думаєте , він буде виробляти, або що ви думаєте , він повинен виробляти, але код буде давати вам саме результати необхідні.


Але з великим застереженням; компілятор вільний припускати відсутність визначеної поведінки (у цьому випадку переповнення). Це схоже на те, як if (a + 1 < a)можна оптимізувати перевірку на переповнення .
csiz

7
@csiz ... на підписаних змінних. Непідписані змінні мають чітко виражену семантику переповнення (обертання).
Гавін С. Янцей

7

Цитуючи з стандарти :

[Примітка: Оператори можуть бути перегруповані за звичайними математичними правилами лише там, де оператори дійсно асоціативні чи комутативні.7 Наприклад, у наступному фрагменті int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

вираз висловлювання поводиться точно так само, як

a = (((a + 32760) + b) + 5);

через асоціативність та перевагу цих операторів. Таким чином, результат суми (a + 32760) далі додається до b, і цей результат потім додається до 5, що призводить до значення, присвоєного a. На машині, в якій переповнення створюють виняток і в якому діапазон значень, представлених int, є [-32768, + 32767], реалізація не може переписати цей вираз як

a = ((a + b) + 32765);

оскільки якби значення для a і b були відповідно -32754 і -15, сума a + b створила б виняток, тоді як початковий вираз не буде; а також вираз не може бути переписаний як

a = ((a + 32765) + b);

або

a = (a + (b + 32765));

оскільки значення для a і b могли бути відповідно 4 і -8 або -17 і 12. Однак на машині, в якій переповнення не створюють винятку і в яких результати переповнення є оборотними, вищевикладене твердження виразів може повинні бути переписані реалізацією будь-яким із перерахованих вище способів, оскільки відбудеться той самий результат. - кінцева примітка]


4

Чи дозволяється компіляторам проводити таке переупорядкування чи ми можемо їм довіряти помічати результат невідповідності та зберігати порядок виразів таким, яким є?

Компілятор може змінити порядок, лише якщо він дає такий же результат - тут, як ви зауважили, він цього не робить.


Можна написати шаблон функції, якщо вам потрібен той, який просуває всі аргументи до std::common_typeдодавання - це було б безпечно, і не покладатися ні на порядок аргументів, ні на ручне кастинг, але це досить незграбно.


Я знаю, що явне кастинг слід використовувати, але я хочу знати поведінку компіляторів, коли таке кастинг було помилково пропущено.
Тал

1
Як я вже сказав, без явного кастингу: ліве додавання виконується спочатку, без цілісного просування, а тому підлягає обгортанню. Результат цього того, можливо , загорнуті, в той підвищений до uint64_tдля додавання до самого правого значенням.
Марно

Твоє пояснення щодо правила-ніби абсолютно неправильне. Наприклад, мова C вказує, які операції повинні відбуватися на абстрактній машині. Правило "як ніби" дозволяє йому робити абсолютно все, що завгодно, доки ніхто не може сказати різницю.
gnasher729

Що означає, що компілятор може робити все, що завгодно, доки результат буде таким самим, як той, який визначається лівими асоціативними та арифметичними правилами перетворення.
Марно

1

Це залежить від бітової ширини unsigned/int.

Нижче 2 не однакові (коли unsigned <= 32біти). u32_x + u32_yстає 0.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

Вони однакові (коли unsigned >= 34біти). Акції цілої кількості спричинили u32_x + u32_yдодавання до 64-бітної математики. Порядок не має значення.

Це UB (коли unsigned == 33біт). Промоції цілочисень спричинили додавання при підписаному 33-бітовому математиці та переповненому підписом UB.

Чи дозволяється компіляторам робити таке переупорядкування ...?

(32 біта математика): так Re порядку, але таке ж результати повинна відбутися, так не , що зміна порядку OP пропонує. Нижче такі ж

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... чи можемо ми довіряти їм помітити результат непослідовність і зберегти порядок вираження таким, яким він є?

Довіряйте так, але мета кодування OP не є чітко зрозумілою. Чи слід u32_x + u32_yбрати внесок? Якщо ОП хоче цього внеску, код повинен бути

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

Але ні

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