Powtórne porównanie punktów zmiennoprzecinkowych
Ten temat pojawił się wiele razy w StackOverflow, ale wierzę, że jest to nowe podejście. Tak, przeczytałemArtykuły Bruce'a Dawsona iCo powinien wiedzieć każdy informatyk na temat arytmetyki zmiennoprzecinkowej ita miła odpowiedź.
Jak rozumiem, w typowym systemie istnieją cztery podstawowe problemy przy porównywaniu liczb zmiennoprzecinkowych dla równości:
Obliczenia zmiennoprzecinkowe nie są dokładneCzya-b
jest „mały” zależy od skalia
ib
Czya-b
jest „mały” zależy od typua
ib
(np. float, double, long double)Punkt zmiennopozycyjny zazwyczaj ma + -wysokość, NaN i zdenormalizowane reprezentacje, z których każda może zakłócać naiwną formułęTa odpowiedź -- znany jako. „podejście Google” - wydaje się popularne. Obsługuje wszystkie trudne przypadki. I bardzo precyzyjnie skaluje porównanie, sprawdzając, czy dwie wartości mieszczą się w ustalonej liczbieULP siebie nawzajem. Na przykład bardzo duża liczba porównuje „prawie równe” do nieskończoności.
Jednak:
Moim zdaniem jest bardzo brudny.Nie jest szczególnie przenośny, opierając się w dużym stopniu na wewnętrznych reprezentacjach, używając unii do odczytu bitów z pływaka itp.Obsługuje tylko jedną precyzję IEEE 754 o podwójnej precyzji (w szczególności nie ma podwójnego podwójnego x86)Chcę czegoś podobnego, ale używając standardowego C ++ i obsługując długie deble. Przez „standard” rozumiem C ++ 03, jeśli to możliwe, i C ++ 11, jeśli to konieczne.
Oto moja próba.
#include <cmath>
#include <limits>
#include <algorithm>
namespace {
// Local version of frexp() that handles infinities specially.
template<typename T>
T my_frexp(const T num, int *exp)
{
typedef std::numeric_limits<T> limits;
// Treat +-infinity as +-(2^max_exponent).
if (std::abs(num) > limits::max())
{
*exp = limits::max_exponent + 1;
return std::copysign(0.5, num);
}
else return std::frexp(num, exp);
}
}
template<typename T>
bool almostEqual(const T a, const T b, const unsigned ulps=4)
{
// Handle NaN.
if (std::isnan(a) || std::isnan(b))
return false;
typedef std::numeric_limits<T> limits;
// Handle very small and exactly equal values.
if (std::abs(a-b) <= ulps * limits::denorm_min())
return true;
// frexp() does the wrong thing for zero. But if we get this far
// and either number is zero, then the other is too big, so just
// handle that now.
if (a == 0 || b == 0)
return false;
// Break the numbers into significand and exponent, sorting them by
// exponent.
int min_exp, max_exp;
T min_frac = my_frexp(a, &min_exp);
T max_frac = my_frexp(b, &max_exp);
if (min_exp > max_exp)
{
std::swap(min_frac, max_frac);
std::swap(min_exp, max_exp);
}
// Convert the smaller to the scale of the larger by adjusting its
// significand.
const T scaled_min_frac = std::ldexp(min_frac, min_exp-max_exp);
// Since the significands are now in the same scale, and the larger
// is in the range [0.5, 1), 1 ulp is just epsilon/2.
return std::abs(max_frac-scaled_min_frac) <= ulps * limits::epsilon() / 2;
}
Twierdzę, że ten kod (a) obsługuje wszystkie istotne przypadki, (b) robi to samo, co implementacja Google dla IEEE-754 z pojedynczą i podwójną precyzją, i (c) jest doskonale standardowy C ++.
Jedno lub więcej z tych twierdzeń jest prawie na pewno błędne. Przyjmę każdą odpowiedź, która to pokazuje, najlepiej z poprawką. Dobra odpowiedź powinna obejmować co najmniej jeden z następujących elementów:
Konkretne dane wejściowe różniące się o więcej niżulps
Jednostki w ostatnim miejscu, ale dla których ta funkcja zwraca wartość true (im większa różnica, tym lepiej)Konkretne dane wejściowe różniące się o maksymalnieulps
Jednostki w ostatnim miejscu, ale dla których ta funkcja zwraca false (im mniejsza różnica, tym lepiej)Wszelkie sprawy, które przegapiłemDowolny sposób, w jaki ten kod opiera się na niezdefiniowanym zachowaniu lub przerwach w zależności od zachowania określonego przez implementację. (Jeśli to możliwe, proszę podać odpowiednią specyfikację)Rozwiązuje problemy, które identyfikujeszDowolny sposób na uproszczenie kodu bez przerywania goZamierzam postawić na to pytanie niebanalną nagrodę.