Poniższy kod działa w programie Visual Studio 2008 z optymalizacją i bez niej. Ale działa tylko na g ++ bez optymalizacji (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;
}
Wynik powinien być:
4.5
4.6
Ale g ++ z optymalizacją ( O1
- O3
) wyświetli:
4.5
4.5
Jeśli dodam volatile
słowo kluczowe przed t, to działa, więc czy może wystąpić jakiś błąd optymalizacji?
Test na g ++ 4.1.2 i 4.4.4.
Oto wynik na ideone: http://ideone.com/Rz937
Opcja, którą testuję na g ++, jest prosta:
g++ -O2 round.cpp
Ciekawszy wynik, nawet jeśli włączę /fp:fast
opcję na Visual Studio 2008, wynik nadal jest poprawny.
Kolejne pytanie:
Zastanawiałem się, czy zawsze powinienem włączać -ffloat-store
opcję?
Ponieważ testowana przeze mnie wersja g ++ jest dostarczana z CentOS / Red Hat Linux 5 i CentOS / Redhat 6 .
Skompilowałem wiele moich programów na tych platformach i obawiam się, że spowoduje to nieoczekiwane błędy w moich programach. Wydaje się, że trochę trudno jest zbadać cały mój kod C ++ i używane biblioteki, czy mają takie problemy. Jakieś sugestie?
Czy ktoś jest zainteresowany tym, dlaczego nawet /fp:fast
włączony, Visual Studio 2008 nadal działa? Wygląda na to, że Visual Studio 2008 jest bardziej niezawodny w tym problemie niż g ++?
źródło
Odpowiedzi:
Procesory Intel x86 używają wewnętrznie 80-bitowej rozszerzonej precyzji, podczas gdy
double
zwykle mają 64-bitową szerokość. Różne poziomy optymalizacji wpływają na to, jak często wartości zmiennoprzecinkowe z procesora są zapisywane w pamięci, a tym samym zaokrąglane z precyzji 80-bitowej do precyzji 64-bitowej.Użyj
-ffloat-store
opcji gcc, aby uzyskać te same wyniki zmiennoprzecinkowe z różnymi poziomami optymalizacji.Alternatywnie, użyj
long double
typu, który zwykle ma 80-bitową szerokość w gcc, aby uniknąć zaokrąglania z 80-bitowej do 64-bitowej precyzji.man gcc
mówi wszystko:W kompilacjach x86_64 kompilatory używają rejestrów SSE dla
float
idouble
domyślnie, dzięki czemu nie jest używana rozszerzona precyzja i ten problem nie występuje.gcc
opcja kompilatora-mfpmath
kontroluje to.źródło
inf
. Nie ma praktycznej zasady, testy jednostkowe mogą dać jednoznaczną odpowiedź.Jak Maxim Yegorushkin już zauważył w swojej odpowiedzi, część problemu polega na tym, że wewnętrznie twój komputer używa 80-bitowej reprezentacji zmiennoprzecinkowej. To tylko część problemu. Podstawą problemu jest to, że żadna liczba w postaci n.nn5 nie ma dokładnej binarnej reprezentacji zmiennoprzecinkowej. Te przypadki narożne są zawsze niedokładnymi liczbami.
Jeśli naprawdę chcesz, aby Twoje zaokrąglanie mogło niezawodnie zaokrąglić te przypadki narożne, potrzebujesz algorytmu zaokrąglania, który uwzględnia fakt, że n.n5, n.nn5 lub n.nnn5 itd. (Ale nie n.5) jest zawsze niedokładny. Znajdź przypadek narożny, który określa, czy jakaś wartość wejściowa jest zaokrąglana w górę, czy w dół, i zwraca wartość zaokrągloną w górę lub w dół na podstawie porównania z tym przypadkiem narożnym. I musisz uważać, aby optymalizujący kompilator nie umieścił tego znalezionego narożnika w rejestrze o rozszerzonej precyzji.
Zobacz, w jaki sposób program Excel pomyślnie zaokrągla liczby zmienne, mimo że są one nieprecyzyjne? dla takiego algorytmu.
Lub możesz po prostu żyć z faktem, że narożniki czasami będą błędnie zaokrąglane.
źródło
Różne kompilatory mają różne ustawienia optymalizacji. Niektóre z szybszych ustawień optymalizacji nie zachowują ścisłych reguł zmiennoprzecinkowych zgodnie z IEEE 754 . Visual Studio ma specyficzne ustawienie,
/fp:strict
,/fp:precise
,/fp:fast
, gdzie/fp:fast
jest niezgodny ze standardem na to, co można zrobić. Może się okazać, że ta flaga steruje optymalizacją w takich ustawieniach. Możesz również znaleźć podobne ustawienie w GCC, które zmienia zachowanie.Jeśli tak jest, jedyną różnicą między kompilatorami jest to, że GCC domyślnie szukałby najszybszego zachowania zmiennoprzecinkowego przy wyższych optymalizacjach, podczas gdy Visual Studio nie zmienia zachowania zmiennoprzecinkowego przy wyższych poziomach optymalizacji. Dlatego niekoniecznie musi to być rzeczywisty błąd, ale zamierzone zachowanie opcji, o której nie wiedziałeś, że włączasz.
źródło
-ffast-math
przełącznik dla GCC, który nie jest włączany przez żaden z-O
poziomów optymalizacji od czasu cytatu: „może to spowodować nieprawidłowe wyjście dla programów, które zależą od dokładnej implementacji reguł / specyfikacji IEEE lub ISO dla funkcji matematycznych”.-ffast-math
i kilka innych rzeczy na moimg++ 4.4.3
i nadal nie mogę odtworzyć problemu.-ffast-math
otrzymuję4.5
poziomy optymalizacji większe niż0
.4.5
z-O1
a-O2
, ale nie z-O0
a-O3
w GCC 4.4.3, ale-O1,2,3
w GCC 4.6.1.)Oznacza to, że problem jest związany z instrukcjami debugowania. Wygląda na to, że wystąpił błąd zaokrąglania spowodowany ładowaniem wartości do rejestrów podczas instrukcji wyjściowych, dlatego inni odkryli, że można to naprawić za pomocą
-ffloat-store
Aby być niepoważny, to musi być jakiś powód, że niektórzy programiści nie włączyć
-ffloat-store
, w przeciwnym razie opcja nie istnieje (podobnie, tam musi być jakiś powód, że niektórzy programiści nie włącza się-ffloat-store
). Nie radziłbym zawsze go włączać lub wyłączać. Włączenie go zapobiega niektórym optymalizacjom, ale wyłączenie go pozwala na zachowanie, które otrzymujesz.Ale ogólnie rzecz biorąc, istnieje pewne niedopasowanie między binarnymi liczbami zmiennoprzecinkowymi (używanymi przez komputer) a dziesiętnymi liczbami zmiennoprzecinkowymi (które ludzie są zaznajomieni), a ta niezgodność może powodować podobne zachowanie do tego, co otrzymujesz (aby było jasne, zachowanie które otrzymujesz, nie jest spowodowane niedopasowaniem, ale podobne zachowanie może być). Rzecz w tym, że skoro masz już pewne niejasności, gdy masz do czynienia z zmiennoprzecinkowymi, nie mogę powiedzieć,
-ffloat-store
że to poprawi lub pogorszy.Zamiast tego możesz przyjrzeć się innym rozwiązaniom problemu, który próbujesz rozwiązać (niestety, Koenig nie wskazuje na rzeczywisty artykuł, a ja nie mogę znaleźć dla niego oczywistego „kanonicznego” miejsca, więc będę musiał wysłać Cię do Google ).
Jeśli nie zaokrąglasz w celach wyjściowych, prawdopodobnie spojrzałbym na
std::modf()
(incmath
) istd::numeric_limits<double>::epsilon()
(inlimits
). Zastanawiając się nad oryginalnąround()
funkcją, uważam, że czystsze byłoby zastąpienie wywołaniastd::floor(d + .5)
funkcji wywołaniem tej funkcji:Myślę, że sugeruje to następującą poprawę:
Prosta uwaga:
std::numeric_limits<T>::epsilon()
jest definiowana jako „najmniejsza liczba dodana do 1, która tworzy liczbę różną od 1”. Zwykle musisz użyć względnego epsilon (tj. Skalować epsilon w jakiś sposób, aby uwzględnić fakt, że pracujesz z liczbami innymi niż „1”). Sumad
,.5
istd::numeric_limits<double>::epsilon()
powinien być bliski 1, więc grupowanie Oznacza to ponadto, żestd::numeric_limits<double>::epsilon()
będzie o odpowiedniej wielkości za to, co robimy. Jeśli już,std::numeric_limits<double>::epsilon()
będzie za duża (gdy suma wszystkich trzech jest mniejsza niż jeden) i może spowodować, że będziemy zaokrąglać niektóre liczby w górę, gdy nie powinniśmy.W dzisiejszych czasach powinieneś to rozważyć
std::nearbyint()
.źródło
x - nextafter(x, INFINITY)
jest powiązany z 1 ulp dla x (ale nie używaj tego; jestem pewien, że istnieją przypadki narożne i właśnie to wymyśliłem). Przykład cppreference dlaepsilon()
ma przykład skalowania go w celu uzyskania względnego błędu opartego na ULP .-ffloat-store
: nie używaj w pierwszej kolejności x87. Użyj matematyki SSE2 (64-bitowe pliki binarne lub-mfpmath=sse -msse2
do tworzenia starych, 32-bitowych plików binarnych), ponieważ SSE / SSE2 ma tymczasowe pliki bez dodatkowej precyzji.double
afloat
zmienne w rejestrach XMM są tak naprawdę w 64-bitowym lub 32-bitowym formacie IEEE. (W przeciwieństwie do x87, gdzie rejestry są zawsze 80-bitowe, a przechowywanie w pamięci zaokrągla się do 32 lub 64 bitów.)Zaakceptowana odpowiedź jest poprawna, jeśli kompilujesz do celu x86, który nie zawiera SSE2. Wszystkie nowoczesne procesory x86 obsługują SSE2, więc jeśli możesz z tego skorzystać, powinieneś:
Rozbijmy to.
-mfpmath=sse -msse2
. To wykonuje zaokrąglanie przy użyciu rejestrów SSE2, co jest znacznie szybsze niż przechowywanie każdego pośredniego wyniku w pamięci. Zauważ, że jest to już domyślne ustawienie w GCC dla x86-64. Z wiki GCC :-ffp-contract=off
. Jednak kontrolowanie zaokrąglania nie wystarczy, aby uzyskać dokładne dopasowanie. Instrukcje FMA (łączone mnożenie i dodawanie) mogą zmienić zachowanie zaokrąglania w porównaniu z ich nieskondensowanymi odpowiednikami, więc musimy je wyłączyć. Jest to domyślne ustawienie w Clang, a nie GCC. Jak wyjaśnia ta odpowiedź :Wyłączając FMA, otrzymujemy wyniki, które dokładnie pasują do debugowania i wydania, kosztem pewnej wydajności (i dokładności). Nadal możemy korzystać z innych zalet wydajnościowych SSE i AVX.
źródło
Zagłębiłem się bardziej w ten problem i mogę wprowadzić więcej szczegółów. Po pierwsze, dokładne reprezentacje 4,45 i 4,55 zgodnie z gcc na x84_64 są następujące (z libquadmath wypisuje ostatnią precyzję):
Jak powiedział powyżej Maxim , problem wynika z rozmiaru 80 bitów rejestrów FPU.
Ale dlaczego problem nigdy nie występuje w systemie Windows? na IA-32 jednostka FPU x87 została skonfigurowana do używania wewnętrznej precyzji mantysy wynoszącej 53 bity (co odpowiada całkowitemu rozmiarowi 64 bitów:)
double
. W systemach Linux i Mac OS zastosowano domyślną precyzję 64 bitów (odpowiednik całkowitego rozmiaru 80 bitów:)long double
. Zatem problem powinien być możliwy lub nie na tych różnych platformach poprzez zmianę słowa kontrolnego FPU (zakładając, że sekwencja instrukcji wywoła błąd). Problem został zgłoszony do gcc jako błąd 323 (przeczytaj przynajmniej komentarz 92!).Aby pokazać precyzję mantysy w systemie Windows, możesz skompilować to w 32 bitach za pomocą VC ++:
oraz w systemie Linux / Cygwin:
Zauważ, że za pomocą gcc możesz ustawić precyzję FPU
-mpc32/64/80
, chociaż jest ona ignorowana w Cygwin. Ale pamiętaj, że zmieni on rozmiar mantysy, ale nie wykładnika, otwierając drzwi dla innych rodzajów różnych zachowań.W architekturze x86_64, SSE jest używane, jak powiedział tmandry , więc problem nie wystąpi, chyba że wymusisz stary FPU x87 do obliczeń FP
-mfpmath=387
lub jeśli nie będziesz kompilować w trybie 32-bitowym z-m32
(będziesz potrzebować pakietu multilib). Mogę odtworzyć problem w systemie Linux z różnymi kombinacjami flag i wersji gcc:Wypróbowałem kilka kombinacji w systemie Windows lub Cygwin z VC ++ / gcc / tcc, ale błąd nigdy się nie pojawił. Przypuszczam, że sekwencja generowanych instrukcji nie jest taka sama.
Na koniec zwróć uwagę, że egzotycznym sposobem uniknięcia tego problemu z 4.45 lub 4.55 byłoby użycie
_Decimal32/64/128
, ale wsparcie jest naprawdę rzadkie ... Spędziłem dużo czasu tylko po to, aby móc wykonać printf zlibdfp
!źródło
Osobiście napotkałem ten sam problem idąc w drugą stronę - od gcc do VS. W większości przypadków uważam, że lepiej unikać optymalizacji. Jedyny przypadek, w którym jest to warte zachodu, dotyczy metod numerycznych obejmujących duże tablice danych zmiennoprzecinkowych. Nawet po demontażu często jestem rozczarowany wyborami kompilatorów. Bardzo często po prostu łatwiej jest użyć wewnętrznych funkcji kompilatora lub po prostu napisać zestaw samodzielnie.
źródło