Чи дозволена ця оптимізація з плаваючою точкою?


90

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

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

Здається, цей код працює з усіма компіляторами, крім clang. Clang генерує простий нескінченний цикл. Годболт .

Це дозволено? Якщо так, чи це питання якості?


@geza Мені було б цікаво почути отриманий номер!
nada

5
gccробить ту ж саму нескінченну оптимізацію циклів, якщо -Ofastзамість цього ви компілюєте , тому ця оптимізація вважається gccнебезпечною, але вона може це зробити.
12345ieee,

3
g ++ також генерує нескінченний цикл, але це не оптимізує роботу всередині нього. Ви можете бачити, що це робить ucomiss xmm0,xmm0для порівняння (float)iз самим собою. Це був ваш перший підказка, що ваше джерело C ++ не означає те, що ви думали. Ви стверджуєте, що отримали цей цикл для друку / повернення 16777216? З яким компілятором / версією / опціями це було? Тому що це була б помилка компілятора. gcc правильно оптимізує ваш код jnpяк гілку циклу ( godbolt.org/z/XJYWeu ): продовжуйте цикл, доки операнди не != були NaN.
Пітер Кордес,

4
Зокрема, це -ffast-mathопція, яка неявно включена, -Ofastщо дозволяє GCC застосовувати небезпечні оптимізації з плаваючою комою і таким чином генерувати той самий код, що і Clang. MSVC поводиться точно так само: без /fp:fast, він генерує купу коду, що призводить до нескінченного циклу; з /fp:fast, він видає одну jmpінструкцію. Я припускаю, що без явного вмикання небезпечних оптимізацій FP ці компілятори зависають на вимогах IEEE 754 щодо значень NaN. Насправді цікаво, що цього не робить Кланг. Кращий його статичний аналізатор. @ 12345ieee
Коді Грей

1
@geza: Якби код зробив те, що ви задумали, перевіряючи, коли математичне значення (float) iвідхиляється від математичного значення i, тоді результат (значення, що повертається у returnвиписці) буде 16 777 217, а не 16 777 216.
Eric Postpischil

Відповіді:


49

Як зазначав @Angew , !=оператору потрібні однакові типи з обох сторін. (float)i != iпризводить до просування RHS до плаваючого, так що ми маємо (float)i != (float)i.


g ++ також генерує нескінченний цикл, але це не оптимізує роботу всередині нього. Ви можете бачити, як він перетворює int-> float з cvtsi2ssі робить ucomiss xmm0,xmm0для порівняння (float)iз собою. (Це був ваш перший підказка про те, що ваше джерело C ++ не означає те, що ви думали, як це, як пояснює відповідь @ Angew.)

x != xвірно лише тоді, коли це "невпорядковано", оскільки це xбув NaN. ( INFINITYпорівнює рівне собі в математиці IEEE, але NaN - ні. NAN == NANнеправда, NAN != NANістина).

gcc7.4 і старіші правильно оптимізують ваш код jnpяк гілку циклу ( https://godbolt.org/z/fyOhW1 ): продовжуйте цикл, доки операнди не x != x були NaN. (gcc8 і пізніше також перевіряє, jeчи не виходить з циклу, не вдаючись оптимізувати, виходячи з того, що це завжди буде вірно для будь-якого входу, що не є NaN). x86 FP порівнює встановлений PF на невпорядкований.


До речі, це означає , що оптимізація clang також є безпечною : вона просто має CSE (float)i != (implicit conversion to float)iоднаковою і довести, що i -> floatніколи не є NaN для можливого діапазону int.

(Хоча з огляду на те, що цей цикл потрапить на UB із переповненим знаком, дозволено видавати буквально будь-який ASM, який він хоче, включаючи ud2незаконну інструкцію або порожній нескінченний цикл, незалежно від того, яким тілом цикла був насправді.) Але ігноруючи UB із переписаним переповненням. , ця оптимізація все ще на 100% легальна.


GCC не вдається оптимізувати тіло циклу, навіть не -fwrapvроблячи чітко визначеним переповненням підписаних цілих чисел (як обгортку доповнення 2). https://godbolt.org/z/t9A8t_

Навіть увімкнення -fno-trapping-mathне допомагає. (GCC за замовчуванням, на жаль, увімкнено,
-ftrapping-mathнавіть незважаючи на те, що його реалізація GCC порушена / виправлена ​​помилка.) Перетворення int-> float може спричинити неточне виняток FP (для номерів, занадто великих, щоб бути точно представленими), тому за винятками, можливо, розкритими, розумно не робити оптимізувати тіло циклу. (Оскільки перетворення 16777217в float може мати спостережуваний побічний ефект, якщо неточний виняток не маскується.)

Але при -O3 -fwrapv -fno-trapping-mathцьому на 100% пропущена оптимізація, щоб не компілювати це в порожній нескінченний цикл. Без #pragma STDC FENV_ACCESS ONцього стан липких прапорів, що фіксують замасковані винятки FP, не є спостережуваним побічним ефектом коду. Ні int-> floatперетворення може призвести до NaN, тому x != xне може бути правдою.


Усі ці компілятори оптимізовані для реалізацій C ++, які використовують IEEE 754 одноточну (binary32) floatта 32-бітну int.

Виправлена(int)(float)i != i цикл матиме UB на реалізаціях C ++ з вузькими 16-бітами intі / або ширше float, тому що ви потрапили зареєстрований Целочисленное переповнення UB до досягнення першого цілого числа , яке не було точно уявімо як float.

Але UB за іншого набору варіантів, визначених реалізацією, не має негативних наслідків при компіляції для реалізації, як gcc або clang, із системою x86-64 System V ABI.


До речі, ви можете статично обчислити результат цього циклу з FLT_RADIXі FLT_MANT_DIG, визначеного в <climits>. Або, принаймні, ви можете теоретично, якщо floatнасправді відповідає моделі плаваючого модуля IEEE, а не якомусь іншому виду подання реального числа, такому як Posit / unum.

Я не впевнений, наскільки стандарт ISO C ++ нагадує про floatповедінку та чи формат, який не базувався на показнику експоненти та значеннях фіксованої ширини, відповідав би стандартам.


У коментарях:

@geza Мені було б цікаво почути отриманий номер!

@nada: це 16777216

Ви стверджуєте, що отримали цей цикл для друку / повернення 16777216?

Оновлення: оскільки цей коментар було видалено, я думаю, ні. Можливо, OP просто цитує floatперед першим цілим числом, яке не може бути точно представлене як 32-бітове float. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values, тобто те, що вони сподівалися перевірити за допомогою цього коду глюки.

З виправленою помилкою версія, звичайно, друкує 16777217перше ціле число, яке не є точно репрезентативним, а не значення перед цим.

(Усі вищі значення з плаваючою точкою є цілими числами, але вони кратні 2, потім 4, потім 8 і т. Д. Для значень експоненти, що перевищують значення і ширину. Можна представити багато вищих цілих значень, але 1 одиниця в останньому місці (значущого) більше 1, тому вони не є суміжними цілими числами. Найбільше скінченне floatтрохи нижче 2 ^ 128, що занадто велике для парних int64_t.

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


3
@SombreroChicken: ні, спочатку я навчився електроніці (з деяких підручників лежав мій тато; він був професором фізики), потім цифровій логіці і після цього потрапив у процесори / програмне забезпечення. : P Отже, мені майже завжди подобалося розуміти речі з нуля, або якщо я починаю з більш високого рівня, то мені подобається дізнатися хоча б щось про рівень нижче, що впливає на те, як / чому все працює на тому рівні, який я думати про. (наприклад, на те, як працює asm і як його оптимізувати, впливають обмеження конструкції процесора / архітектура процесора. Що, в свою чергу, походить від фізики + математики.)
Пітер Кордес,

1
GCC, можливо, не зможе оптимізувати навіть frapw, але я впевнений, що GCC 10 -ffinite-loopsбув розроблений для таких ситуацій.
MCCCS

64

Зверніть увагу, що вбудований оператор !=вимагає, щоб його операнди були однотипними, і досягне цього за допомогою рекламних акцій та перетворень, якщо це необхідно. Іншими словами, ваш стан еквівалентний:

(float)i != (float)i

Це ніколи не повинно провалитися, і тому код врешті-решт переллється i, надаючи вашій програмі невизначену поведінку. Тому можлива будь-яка поведінка.

Щоб правильно перевірити те, що ви хочете перевірити, слід повернути результат назад до int:

if ((int)(float)i != i)

8
@ Džuris Це UB. Там немає не один певного результату. Компілятор може зрозуміти, що це може закінчитися лише UB і вирішити повністю видалити цикл.
Позов до Моніки

4
@opa ви маєте на увазі static_cast<int>(static_cast<float>(i))? reinterpret_castочевидно UB там
Калет

6
@NicHartley: Ви хочете сказати, що (int)(float)i != iце UB? З чого ви робите висновок? Так, це залежить від властивостей, визначених реалізацією (оскільки floatне обов’язково бути двійковим файлом IEEE754), але для будь-якої даної реалізації вона є чітко визначеною, якщо не floatможе точно представляти всі позитивні intзначення, тому ми отримуємо UB із переповненням із цілим числом. ( en.cppreference.com/w/cpp/types/climits визначає FLT_RADIXта FLT_MANT_DIGвизначає це). Загалом речі, визначені реалізацією друку, начебто std::cout << sizeof(int)не UB ...
Пітер Кордес,

2
@Caleth: reinterpret_cast<int>(float)це не зовсім UB, це просто синтаксична помилка / неправильно сформована. Було б непогано, якби цей синтаксис дозволяв набирати шрифти float до intяк альтернативу memcpy(що є чітко визначеним), але reinterpret_cast<>, думаю , він працює лише на типи покажчиків.
Пітер Кордес,

2
@Peter Тільки для NaN, x != xце правда. Дивіться у прямому ефірі на коліру . У С теж.
Deduplicator
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.