2017-01-24 137 views
8

Przeszukałem niektóre z natywnych kodu źródłowego funkcji języka Java Math. Zwłaszcza tanh(), ponieważ byłem ciekawy, jak go zaimplementowali. Jednak what I found zaskoczył mnie:Java/C: Zła implementacja OpenJDK native tanh()?

double tanh(double x) { 
    ... 
    if (ix < 0x40360000) {   /* |x|<22 */ 
     if (ix<0x3c800000)   /* |x|<2**-55 */ 
      return x*(one+x);  /* tanh(small) = small */ 
    ... 
} 

Jako komentarz wskazuje, taylor series of tanh(x) around 0, rozpoczyna się:

tanh(x) = x - x^3/3 + ... 

to dlaczego to wygląda one realizowane jako:

tanh(x) = x * (1 + x) 
     = x + x^2 

Co najwyraźniej nie jest prawidłową ekspansją, a nawet gorszym przybliżeniem niż użycie tylko tanh(x) = x (co byłoby szybsze), co wskazuje na tym wykresie:

enter image description here

(Linia pogrubiona jedną wskazane na szczycie. Drugi szary to log(abs(x(1+x) - tanh(x))). Sigmoid to oczywiście sama tanh(x)).

Czy jest to błąd w implementacji, czy jest to hack, aby naprawić jakiś problem (np. Problemy numeryczne, o których naprawdę nie myślę)? Zauważ, że spodziewam się, że wynik obu podejść będzie dokładnie taki sam, jak nie ma wystarczającej liczby bitów mantisse, aby faktycznie wykonać dodawanie 1 + x, dla x < 2^(- 55).

EDIT: I will include a link to the version of the code at the time of writing, for future reference, as this might get fixed.

+1

Ten rodzaj gówna powoduje eksplozje rakiet. –

+1

W pełni oczekuj, że 'tanh()' będzie funkcją _odd_, więc 'y = f (x) -> y = -f (-x)'. 'x + x^2' to powoduje. Jedyna myśl polega na tym, że może wymusić znak + na 'f (-0.0)', ale to jest łatwe do zrobienia z 'tanh (x) = x + 0.0;'. IMO, błąd, który mógł nie objawiać się jako '| x | <2 ** - 55' ... lub coś związanego z zaokrąglaniem flag. – chux

+0

Czy 'x * (jeden + x)' może być trudnym sposobem na uzyskanie 'x + 0.0' na docelowej platformie? – chux

Odpowiedz

6

W warunkach, w których, że kod jest wykonywany, a zakładając, że IEEE-754 podwójnej precyzji przestawne reprezentacje zwrotnicowe i arytmetyka są w użyciu, 1.0 + x zawsze oceniać na 1.0, więc x * (1.0 + x) zawsze oceni na x. Jedynym zewnętrznym (do funkcji) obserwowalnym efektem wykonywania obliczeń, jak to się robi, a nie tylko zwracaniem x byłoby ustawienie flagi statusu "nieścisłej" IEEE.

Mimo że nie ma możliwości sprawdzenia flag statusu FP z Javy, inne natywne kody mogłyby je wywoływać. Bardziej prawdopodobne niż nie, jednak praktyczny powód, dla realizacji podany jest przez tych uwag w the Javadocs for java.StrictMath:

Aby zapewnić przenośność programów Java, definicje niektórych funkcji liczbowych w tym pakiecie wymagają one dają takie same wyniki, jak niektóre opublikowane algorytmy. Algorytmy te są dostępne z dobrze znanej biblioteki sieciowej netlib jako pakiet "Freely Distributable Math Library", fdlibm. Algorytmy te, które są napisane w języku programowania C, należy następnie rozumieć jako wykonywane ze wszystkimi operacjami zmiennoprzecinkowymi zgodnie z regułami arytmetyki zmiennoprzecinkowej Java.

Biblioteka matematyczna Java jest zdefiniowana w odniesieniu do wersji 5.3 programu fdlibm. Gdzie fdlibm dostarcza więcej niż jedną definicję funkcji (np. Acos), użyj wersji "IEEE 754 core" (rezydującej w pliku, którego nazwa zaczyna się na literę e).Metody wymagające fdlibm semantyka sin, cos, tan, asin, acos, atan, exp, log, log10, cbrt, atan2, pow, sinh, cosh, tanh, hypot, expm1 i log1p.

(Podkreślenie dodano.) W kodzie źródłowym C pojawi się #include "fdlibm.h", który wydaje się powiązać go z komentarzami Javadoc.

+1

Zauważ, że zakres | x | <2 ** (- 55) zawiera denormalne (podnormalne) operandy, a zatem ten kod może również podnosić flagi denormalne i niedopełnione, oprócz flagi niedokładności. Powodem, dla którego nie można użyć 'x * 1.0', jest to, że w ramach wiązań IEEE-754 dla ISO C można to zoptymalizować do prostego' x' (sekcja F.8.2 ISO C99), a takie przypisanie nie wzbudziłoby żadnego pływającego flagi punktów. – njuffa

+0

@njuffa Pomyślałem, że flaga niedopełnienia została podniesiona tylko wtedy, gdy wynik operacji był niedokładny i mały, aby ta flaga nigdy nie została podniesiona o "1.0 * x"? –

+0

@PascalCuoq Dobra rada, możesz mieć rację. Jeśli tak, 'x * 1.0' może nie być odpowiedni niezależnie od optymalizacji kompilatorów. Nie pamiętam wszystkich szczegółów podnoszenia flagi niedopełnienia (ta jest najbardziej skomplikowana), ale wydaje mi się, że pamiętam, że IEEE 754-1985 (co jest istotne dla kodu FDLIBM) umożliwiło alternatywne projekty dla czynników przyczyniających się do "tininess" oraz "utratę dokładności", gdzie to pierwsze można było wykryć przed lub po zaokrągleniu, a to ostatnie niekoniecznie wiązało się z niedokładnością. – njuffa