Wersja wbudowana funkcji zwraca inną wartość niż wersja niezawierająca

85

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
zbrojny120
źródło
3
Czy możesz podać jakiego kompilatora, opcji kompilatora używasz i jakiej maszyny? U mnie działa dobrze na GCC 7.1 w systemie Windows.
Diodacus
31
Czy nie ==zawsze jest trochę nieprzewidywalne w przypadku wartości zmiennoprzecinkowych?
500 - Wewnętrzny błąd serwera
3
powiązany stackoverflow.com/questions/588004/…
idclev 463035818
2
Czy ustawiłeś -Ofastopcję, która umożliwia takie optymalizacje?
cmdLP
4
Kompilator zwraca cbrt(27.0)wartość, 0x0000000000000840podczas gdy biblioteka standardowa zwraca 0x0100000000000840. 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ę.
KamilCuk

Odpowiedzi:

73

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:

  • Złożoność wyrażenia
  • Próg używany przez kompilator jako wartość odcięcia podczas próby wykonania oceny czasu kompilacji
  • Inne heurystyki używane w szczególnych przypadkach (np. Gdy pętle clang elides)

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.

NB: W przykładach kompilatora-eksploratora używam printfzamiast tego iostream, ponieważ zmniejsza złożoność funkcji głównej, dzięki czemu efekt jest bardziej widoczny.

Wykazanie, że inlinenie ma to wpływu na ocenę środowiska uruchomieniowego

Moż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.

J. Antonio Perez
źródło
22

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

PW
źródło
Jaki jest cel max()rozmowy? Z definicji floor(x)jest mniejsze lub równe x, więc max(x, floor(x))zawsze będzie równe x.
Ken Thomases,
@KenThomases: W tym konkretnym przypadku, gdy jeden argument maxjest tylko floordrugim, nie jest to wymagane. Rozważyłem jednak ogólny przypadek, w którym argumenty maxmogą być wartościami lub wyrażeniami, które są od siebie niezależne.
PW
Nie należy tego 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.
Peter - Przywróć Monikę
Myślę, że lepiej będzie, jeśli użytkownik będzie mógł określić Epsilonwartość w zależności od ich konkretnych wymagań.
PW