W jaki sposób dwie wersje tej samej funkcji, różniące się tylko tym, że jedna jest wbudowana, a druga nie, mogą zwracać różne wartości? Oto kod, który napisałem dzisiaj i nie jestem pewien, jak to działa.
#include <cmath>
#include <iostream>
bool is_cube(double r)
{
return floor(cbrt(r)) == cbrt(r);
}
bool inline is_cube_inline(double r)
{
return floor(cbrt(r)) == cbrt(r);
}
int main()
{
std::cout << (floor(cbrt(27.0)) == cbrt(27.0)) << std::endl;
std::cout << (is_cube(27.0)) << std::endl;
std::cout << (is_cube_inline(27.0)) << std::endl;
}
Spodziewałbym się, że wszystkie wyniki będą równe 1
, ale w rzeczywistości wyświetla to (g ++ 8.3.1, bez flag):
1
0
1
zamiast
1
1
1
Edycja: clang ++ 7.0.0 wyświetla to:
0
0
0
i g ++ - Szybko to:
1
1
1
==
zawsze jest trochę nieprzewidywalne w przypadku wartości zmiennoprzecinkowych?-Ofast
opcję, która umożliwia takie optymalizacje?cbrt(27.0)
wartość,0x0000000000000840
podczas gdy biblioteka standardowa zwraca0x0100000000000840
. Podwójne liczby różnią się szesnastą liczbą po przecinku. Mój system: archlinux4.20 x64 gcc8.2.1 glibc2.28 Sprawdzone w tym . Ciekawe, czy gcc lub glibc mają rację.Odpowiedzi:
Wyjaśnienie
Niektóre kompilatory (zwłaszcza GCC) używają większej precyzji podczas oceny wyrażeń w czasie kompilacji. Jeśli wyrażenie zależy tylko od stałych danych wejściowych i literałów, może zostać ocenione w czasie kompilacji, nawet jeśli wyrażenie nie jest przypisane do zmiennej constexpr. To, czy tak się stanie, zależy od:
Jeśli wyrażenie jest jawnie podane, tak jak w pierwszym przypadku, ma ono mniejszą złożoność i kompilator prawdopodobnie oceni je w czasie kompilacji.
Podobnie, jeśli funkcja jest oznaczona jako wbudowana, kompilator z większym prawdopodobieństwem oceni ją w czasie kompilacji, ponieważ funkcje wbudowane podnoszą próg, przy którym może wystąpić ocena.
Wyższe poziomy optymalizacji również zwiększają ten próg, jak w przykładzie -Ofast, w którym wszystkie wyrażenia są oceniane jako prawda na gcc ze względu na wyższą precyzję obliczania czasu kompilacji.
Takie zachowanie możemy zaobserwować tutaj w eksploratorze kompilatora. Po skompilowaniu z -O1 tylko funkcja zaznaczona jako inline jest oceniana w czasie kompilacji, ale przy -O3 obie funkcje są oceniane w czasie kompilacji.
-O1
: https://godbolt.org/z/u4gh0g-O3
: https://godbolt.org/z/nVK4SoNB: W przykładach kompilatora-eksploratora używam
printf
zamiast tego iostream, ponieważ zmniejsza złożoność funkcji głównej, dzięki czemu efekt jest bardziej widoczny.Wykazanie, że
inline
nie ma to wpływu na ocenę środowiska uruchomieniowegoMożemy zapewnić, że żadne z wyrażeń nie jest oceniane w czasie kompilacji, uzyskując wartość ze standardowego wejścia, a kiedy to robimy, wszystkie 3 wyrażenia zwracają fałsz, jak pokazano tutaj: https://ideone.com/QZbv6X
#include <cmath> #include <iostream> bool is_cube(double r) { return floor(cbrt(r)) == cbrt(r); } bool inline is_cube_inline(double r) { return floor(cbrt(r)) == cbrt(r); } int main() { double value; std::cin >> value; std::cout << (floor(cbrt(value)) == cbrt(value)) << std::endl; // false std::cout << (is_cube(value)) << std::endl; // false std::cout << (is_cube_inline(value)) << std::endl; // false }
W przeciwieństwie do tego przykładu , w którym używamy tych samych ustawień kompilatora, ale podajemy wartość w czasie kompilacji, co skutkuje dokładniejszą oceną czasu kompilacji.
źródło
Jak zaobserwowano, użycie
==
operatora do porównania wartości zmiennoprzecinkowych zaowocowało różnymi danymi wyjściowymi z różnymi kompilatorami i na różnych poziomach optymalizacji.Jednym z dobrych sposobów porównywania wartości zmiennoprzecinkowych jest test tolerancji względnej opisany w artykule: Ponowne sprawdzenie tolerancji zmiennoprzecinkowych .
Najpierw obliczamy wartość
Epsilon
( względnej tolerancji ), która w tym przypadku byłaby:double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();
Następnie użyj go w funkcjach wbudowanych i nieliniowych w ten sposób:
return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon);
Obecnie funkcje to:
bool is_cube(double r) { double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon(); return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon); } bool inline is_cube_inline(double r) { double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon(); return (std::fabs(std::round(std::cbrt(r)) - std::cbrt(r)) < Epsilon); }
Teraz dane wyjściowe będą zgodne z oczekiwaniami (
[1 1 1]
) z różnymi kompilatorami i na różnych poziomach optymalizacji.Demo na żywo
źródło
max()
rozmowy? Z definicjifloor(x)
jest mniejsze lub równex
, więcmax(x, floor(x))
zawsze będzie równex
.max
jest tylkofloor
drugim, nie jest to wymagane. Rozważyłem jednak ogólny przypadek, w którym argumentymax
mogą być wartościami lub wyrażeniami, które są od siebie niezależne.operator==(double, double)
robić dokładnie, sprawdzić, czy różnica jest mniejsza niż przeskalowany epsilon? Około 90% pytań zmiennoprzecinkowych na SO nie istniałoby wtedy.Epsilon
wartość w zależności od ich konkretnych wymagań.