Різні результати з плаваючою комою з увімкненою оптимізацією - помилка компілятора?


109

Наведений нижче код працює на Visual Studio 2008 з оптимізацією та без неї. Але він працює лише на g ++ без оптимізації (O0).

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

Вихід повинен бути:

4.5
4.6

Але g ++ з оптимізацією ( O1- O3) виведе:

4.5
4.5

Якщо я додати volatileключове слово перед t, воно працює, тож може виникнути помилка оптимізації?

Тест на g ++ 4.1.2 та 4.4.4.

Ось результат на ideone: http://ideone.com/Rz937

І варіант, який я тестую на g ++, простий:

g++ -O2 round.cpp

Чим цікавіший результат, навіть якщо я включаю /fp:fastваріант на Visual Studio 2008, результат все-таки правильний.

Подальше запитання:

Мені було цікаво, чи варто завжди включати -ffloat-storeваріант?

Тому що тестована версія g ++ поставляється із CentOS / Red Hat Linux 5 та CentOS / Redhat 6 .

Я склав багато своїх програм під ці платформи, і я переживаю, що це призведе до несподіваних помилок всередині моїх програм. Здається, трохи важко дослідити весь мій код C ++ та використані бібліотеки, чи є у них такі проблеми. Будь-яка пропозиція?

Хтось цікавиться, чому навіть /fp:fastувімкнено, Visual Studio 2008 все ще працює? Здається, Visual Studio 2008 надійніший у цій проблемі, ніж g ++?


51
Для всіх нових користувачів SO: ЦЕ так ви задаєте питання. +1
десять четвертого

1
FWIW, я отримую правильний вихід з g ++ 4.5.0 за допомогою MinGW.
Стів Блеквелл

2
ideone використовує 4.3.4 ideone.com/b8VXg
Daniel A. White

5
Ви повинні пам’ятати, що ваша рутина навряд чи надійно працює з усіма видами продукції. На відміну від округлення подвійного до цілого числа, це вразливо тим, що не всі реальні числа можуть бути представлені, тому слід очікувати, що ви отримаєте більше помилок, як цей.
Якуб Вічерек

2
Тим, хто не може відтворити помилку: не коментуйте коментовані stmts налагодження, вони впливають на результат.
н. 'займенники' м.

Відповіді:


91

Процесори Intel x86 використовують 80-бітну розширену точність всередині, тоді doubleяк зазвичай це 64-розрядна ширина. Різні рівні оптимізації впливають на те, як часто значення плаваючої точки з процесора зберігаються в пам'яті і, таким чином, округляються від 80-бітної точності до 64-бітної точності.

Використовуйте -ffloat-storeопцію gcc, щоб отримати однакові результати з плаваючою точкою з різними рівнями оптимізації.

Крім того, використовуйте long doubleтип, який, як правило, має ширину 80 біт на gcc, щоб уникнути округлення від 80-бітної до 64-бітової точності.

man gcc все це говорить:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.

У складах x86_64 компілятори використовують регістри SSE для floatта doubleза замовчуванням, так що не використовується розширена точність і ця проблема не виникає.

gccПараметр компілятора-mfpmath контролює це.


20
Я думаю, це відповідь. Константа 4.55 перетворюється на 4.54999999999999, що є найближчим бінарним поданням у 64 бітах; помножте на 10 і знову закруглейте на 64 біти і отримаєте 45,5. Якщо ви пропустили крок округлення, зберігаючи його в 80-бітному регістрі, ви отримаєте 45.4999999999999.
Марк Викуп

Дякую, я навіть не знаю цього варіанту. Але мені було цікаво, чи слід завжди вмикати варіант -ffloat-store? Оскільки тестована нами версія g ++ постачається разом з CentOS / Redhat 5 та CentOS / Redhat 6. Я склав багато моїх програм під ці платформи, я переживаю, що це спричинить несподівані помилки у моїх програмах.
Ведмідь

5
@Bear, оператор налагодження, ймовірно, викликає перемикання значення з регістра в пам'ять.
Марк Рансом

2
@ Зверніть увагу, як правило, ваша програма повинна отримувати перевагу від підвищеної точності, якщо вона не працює на надзвичайно крихітних або величезних значеннях, коли очікується, що 64-бітний поплавок буде недостатньо перевантажений або продукується inf. Немає хорошого правила, одиничні тести можуть дати точну відповідь.
Максим Єгорушкін

2
@bear Як правило, якщо вам потрібні результати, цілком передбачувані та / або саме те, що людина отримає, роблячи суми на папері, тоді вам слід уникати плаваючої точки. -ffloat-store видаляє одне джерело непередбачуваності, але це не чарівна куля.
підключення

10

Вихід повинен бути: 4.5 4.6 Ось такий результат був би, якби ви мали нескінченну точність або працювали з пристроєм, який використовував десяткове, а не двійкове подання з плаваючою комою. Але, ти це не так. Більшість комп'ютерів використовують стандарт двійкового IEEE з плаваючою точкою.

Як вже зазначив Максим Єгорушкін у своїй відповіді, частина проблеми полягає в тому, що всередині комп'ютера використовується 80-бітове представлення з плаваючою точкою. Це лише частина проблеми. В основі проблеми лежить те, що будь-яке число форми n.nn5 не має точного двійкового плаваючого подання. Ці кутові випадки - це завжди неточні цифри.

Якщо ви дійсно хочете, щоб ваше округлення могло надійно обігнути ці кутові випадки, вам потрібен алгоритм округлення, який вирішує той факт, що n.n5, n.nn5 або n.nnn5 і т.д. (але не n.5) завжди неточний. Знайдіть кутовий випадок, який визначає, чи деяке вхідне значення округляється вгору або вниз і повертає значення округлення або округлення вниз на основі порівняння з цим кутовим випадком. І вам потрібно подбати про те, щоб оптимізуючий компілятор не помістив знайдений кутовий регістр у розширений регістр точності.

Дивіться, як Excel успішно округляє плаваючі числа, навіть якщо вони неточні? для такого алгоритму.

Або ви можете просто жити з тим, що кутові шафи іноді будуть помилятися помилково.


6

У різних компіляторів є різні налаштування оптимізації. Деякі з цих параметрів швидшої оптимізації не підтримують суворих правил з плаваючою комою відповідно до IEEE 754 . Visual Studio має налаштування конкретних, /fp:strict, /fp:precise, /fp:fast, де /fp:fastпорушує стандарт на те , що може бути зроблено. Ви можете виявити, що саме цей прапор керує оптимізацією в таких налаштуваннях. Ви також можете знайти подібний параметр у GCC, який змінює поведінку.

Якщо це так, то єдине, що відрізняється між компіляторами, - це те, що GCC шукає швидку поведінку з плаваючою точкою за замовчуванням на більш високих оптимізаціях, тоді як Visual Studio не змінює поведінку плаваючої точки з більш високими рівнями оптимізації. Таким чином, це не обов'язково може бути фактичною помилкою, але передбачувана поведінка варіанту, про який ви не знали, увімкнено.


4
Існує -ffast-mathперемикач для GCC, який не вмикається жодним із -Oрівнів оптимізації, оскільки цитую: "це може призвести до неправильного виведення програм, які залежать від точної реалізації IEEE або ISO правил / специфікацій для математичних функцій".
Мат

@Mat: Я спробував -ffast-mathі кілька інших речей, g++ 4.4.3і я все ще не можу відтворити проблему.
NPE

Приємно: -ffast-mathя отримую 4.5в обох випадках рівень оптимізації більше, ніж 0.
Керрек СБ

: (Correction я 4.5з -O1і -O2, але не -O0та -O3в GCC 4.4.3, але -O1,2,3в GCC 4.6.1.)
Kerrek SB

4

Тим, хто не може відтворити помилку: не коментуйте коментовані stmts налагодження, вони впливають на результат.

Це означає, що проблема пов’язана із заявами про налагодження. І, схоже, є помилка округлення, викликана завантаженням значень в регістри під час вихідних операторів, тому інші виявили, що ви можете виправити це за допомогою-ffloat-store

Подальше запитання:

Мені було цікаво, чи варто завжди включати -ffloat-storeваріант?

Для того, щоб бути легковажним, має бути причиною того, що деякі програмісти не включається -ffloat-store, в іншому випадку варіант не існує ( то ж саме, що повинно бути причиною того, що деякі програмісти дійсно включити-ffloat-store ). Я б не рекомендував завжди включати або завжди вимикати його. Увімкнення його запобігає певним оптимізаціям, але вимкнення дозволяє спричинити поведінку.

Але, як правило, існує деяка невідповідність між двійковими числами з плаваючою комою (як, наприклад, комп’ютер) та десятковими числами з плаваючою комою (з якими люди знайомі), і ця невідповідність може спричинити подібну поведінку до тієї, що ви отримуєте (зрозуміло, поведінку) ви отримуєте це НЕ викликано це невідповідність, але схоже поведінку може бути). Річ у тім, оскільки ви вже маєте деяку розпливчастість у роботі з плаваючою точкою, я не можу сказати, що -ffloat-storeце робить кращим або гіршим.

Натомість, ви можете розглянути інші рішення проблеми, яку ви намагаєтеся вирішити (на жаль, Koenig не вказує на фактичний папір, і я не можу реально знайти очевидне "канонічне" місце для цього, тому я доведеться відправити вас в Google ).


Якщо ви не округляєте для цілей виведення, я, мабуть, переглянув std::modf()cmath) і std::numeric_limits<double>::epsilon()limits). Розмірковуючи над початковою round()функцією, я вважаю, що було б більш чітко замінити виклик на std::floor(d + .5)виклик цієї функції:

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

Я думаю, що це говорить про таке покращення:

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

Проста примітка: std::numeric_limits<T>::epsilon()визначається як "найменше число, додане до 1, яке створює число, що не дорівнює 1." Зазвичай вам потрібно використовувати відносний епсилон (тобто масштабний епсілон якось, щоб врахувати той факт, що ви працюєте з числами, відмінними від "1"). Сума d, .5і std::numeric_limits<double>::epsilon()повинна бути близько 1, так що додавання угруповання означає , що std::numeric_limits<double>::epsilon()будуть потрібного розміру для того, що ми робимо. Якщо що-небудь, воно std::numeric_limits<double>::epsilon()буде занадто великим (коли сума всіх трьох менша від одиниці) і може змусити нас округлити деякі числа, коли ми не повинні.


Сьогодні вам слід подумати std::nearbyint().


"Відносним епсилоном" називають 1 ульп (1 одиниця в останньому місці). x - nextafter(x, INFINITY)пов'язаний з 1 ulp для x (але не використовуйте це; я впевнений, що є кутові випадки, і я щойно це склав). Приклад cppreference для epsilon() має приклад масштабування для отримання відносної помилки на основі ULP .
Пітер Кордес

2
До речі, відповідь на 2016 рік -ffloat-store: в першу чергу не використовуйте x87. Використовуйте математику SSE2 (64-бітні двійкові файли або -mfpmath=sse -msse2для виготовлення старих 32-бітних бінарних файлів), оскільки SSE / SSE2 має тимчасові файли без зайвої точності. doubleі floatvars в регістрах XMM дійсно в 64-бітному або 32-бітному форматі IEEE. (На відміну від x87, де регістри завжди є 80-бітними і зберігають у пам'яті раунди до 32 або 64 біт.)
Пітер Кордес

3

Прийнята відповідь правильна, якщо ви збираєтеся до цілі x86, яка не включає SSE2. Всі сучасні процесори x86 підтримують SSE2, тому, якщо ви можете скористатися цим, вам слід:

-mfpmath=sse -msse2 -ffp-contract=off

Давайте розбимо це.

-mfpmath=sse -msse2. Це виконує округлення за допомогою регістрів SSE2, що набагато швидше, ніж зберігання кожного проміжного результату в пам'яті. Зауважте, що це вже за замовчуванням для GCC для x86-64. З вікі GCC :

У більш сучасних процесорах x86, що підтримують SSE2, вказівка ​​параметрів компілятора -mfpmath=sse -msse2забезпечує всі операції з плаваючою і подвійною операціями в регістрах SSE і правильно округлені. Ці параметри не впливають на ABI, і тому їх слід використовувати, коли це можливо, для прогнозованих числових результатів.

-ffp-contract=off. Однак контролювання округлення недостатньо для точної відповідності. Інструкції FMA (злиті множини-додавання) можуть змінити поведінку округлення порівняно зі своїми неплавленими аналогами, тому нам потрібно її відключити. Це за замовчуванням для Clang, а не GCC. Як пояснено у цій відповіді :

FMA має лише одне округлення (воно ефективно зберігає нескінченну точність для внутрішнього результату тимчасового множення), тоді як ADD + MUL - два.

Відключивши FMA, ми отримуємо результати, які точно відповідають налагодженню та випуску, ціною деякої продуктивності (та точності). Ми все ще можемо скористатися іншими перевагами продуктивності SSE та AVX.


1

Я більше занурився в цю проблему і можу зробити більше точок. По-перше, точні зображення 4,45 і 4,55 згідно з gcc на x84_64 наступні (з libquadmath для друку останньої точності):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

Як сказав Максим вище, проблема пов’язана з розміром 80 біт регістрів FPU.

Але чому проблема ніколи не виникає в Windows? на IA-32, F87 X87 був налаштований на використання внутрішньої точності для мантіси 53 біт (еквівалентний загальному розміру 64 біт:) double. Для Linux та Mac OS була використана точність за замовчуванням 64 біта (що еквівалентно загальному розміру 80 біт:) long double. Отже, проблема повинна бути можливою чи ні на цих різних платформах, змінюючи керуюче слово FPU (припускаючи, що послідовність інструкцій викликає помилку). Про проблему було повідомлено gcc як помилка 323 (читайте принаймні коментар 92!).

Щоб показати точність мантіси в Windows, ви можете скласти це в 32 біти за допомогою VC ++:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

та на Linux / Cygwin:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

Зауважте, що за допомогою gcc ви можете встановити точність FPU за допомогою -mpc32/64/80, хоча в Cygwin це ігнорується. Але майте на увазі, що це змінить розмір мантіси, але не показник, дозволяючи двері відчинити для інших видів різної поведінки.

У архітектурі x86_64 використовується SSE, як сказано в tmandry , тому проблема не виникне, якщо ви не змусите старий x87 FPU для обчислення FP -mfpmath=387або, якщо ви не компілюєте в 32- -m32бітовому режимі (вам знадобиться пакет багатолітраж). Я міг би відтворити проблему в Linux за допомогою різних комбінацій прапорів та версій gcc:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

Я спробував кілька комбінацій у Windows або Cygwin з VC ++ / gcc / tcc, але помилка так і не з'явилася. Я думаю, що послідовність створених інструкцій не однакова.

Нарешті, зауважте, що екзотичним способом запобігти цій проблемі з 4.45 або 4.55 було б користуватися _Decimal32/64/128, але підтримка насправді дефіцитна ... Я витратив багато часу просто на те, щоб можна було зробити printf libdfp!


0

Особисто я зіткнувся з тією ж проблемою ідучи іншим шляхом - від gcc до VS. У більшості випадків я вважаю, що краще уникати оптимізації. Єдиний час, коли це варто, коли ви маєте справу з числовими методами, що включають великі масиви даних з плаваючою точкою. Навіть після розбирання я часто переживаю вибір компіляторів. Дуже часто просто простіше користуватися компіляторами або просто писати збірку самостійно.

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