Was ist der Unterschied zwischen java.lang.Math und java.lang.StrictMath?

74

java.lang.StrictMathEnthält natürlich zusätzliche Funktionen (Hyperbel usw.), die java.lang.Mathdies nicht tun, aber gibt es einen Unterschied in den Funktionen, die in beiden Bibliotheken zu finden sind?

Iain Sproat
quelle
6
Diese Frage wird im Javadoc vollständig beantwortet.
Marquis von Lorne
31
@EJP - Ich glaube, dass RTFM auf SO niemals eine gute Antwort ist.
Ripper234

Antworten:

71

Das Javadoc für die MathKlasse enthält einige Informationen zu den Unterschieden zwischen den beiden Klassen:

Im Gegensatz zu einigen numerischen StrictMathKlassenmethoden Mathsind nicht alle Implementierungen der äquivalenten Klassenfunktionen so definiert, dass sie bitweise dieselben Ergebnisse zurückgeben. Diese Entspannung ermöglicht leistungsfähigere Implementierungen, bei denen keine strikte Reproduzierbarkeit erforderlich ist.

Standardmäßig Mathrufen viele Methoden einfach die entsprechende Methode StrictMathfür ihre Implementierung auf. Codegeneratoren werden aufgefordert, plattformspezifische native Bibliotheken oder Mikroprozessoranweisungen zu verwenden, sofern verfügbar, um leistungsstärkere Implementierungen von MathMethoden bereitzustellen . Solche leistungsstärkeren Implementierungen müssen weiterhin der Spezifikation für entsprechen Math.

Daher Mathlegt die Klasse einige Regeln fest, wie bestimmte Operationen ausgeführt werden sollen, verlangt jedoch nicht, dass in allen Implementierungen der Bibliotheken genau dieselben Ergebnisse zurückgegeben werden.

Dies ermöglicht, dass bestimmte Implementierungen der Bibliotheken ähnliche, aber nicht genau das gleiche Ergebnis zurückgeben, wenn beispielsweise die Math.cosKlasse aufgerufen wird. Dies würde plattformspezifische Implementierungen ermöglichen (z. B. die Verwendung von x86-Gleitkomma und beispielsweise SPARC-Gleitkomma), die möglicherweise unterschiedliche Ergebnisse liefern.

( Einige Beispiele für plattformspezifische Implementierungen finden Sie im Abschnitt Software-Implementierungen des Sinus- Artikels in Wikipedia.)

Bei müssen die StrictMathvon verschiedenen Implementierungen zurückgegebenen Ergebnisse jedoch dasselbe Ergebnis zurückgeben. Dies wäre in Fällen wünschenswert, in denen die Reproduzierbarkeit der Ergebnisse auf verschiedenen Plattformen erforderlich ist.

Coobird
quelle
3
Aber warum sollten unterschiedliche plattformspezifische Implementierungen unterschiedliche Ergebnisse erzielen wollen? Ist der Kosinus nicht universell definiert?
Aivar
@Aivar: Aus den im Zitat der MathKlasse aufgeführten Gründen - um die für die jeweilige Plattform verfügbaren nativen Methoden zu nutzen, die (in vielen Fällen wahrscheinlich) schneller sind als die Verwendung einer softwarebasierten Lösung, die dies garantiert Geben Sie auf allen Plattformen genau die gleiche Antwort.
Coobird
ok, das heißt, einige Plattformen haben sich dafür entschieden, nicht die genaueste Antwort zu berechnen, die in bestimmte Mengen von Bits passt, sondern Präzision gegen Effizienz eingetauscht? Und verschiedene Plattformen haben unterschiedliche Kompromisse gemacht?
Aivar
@Aivar Das scheint der Fall zu sein, wenn man den verlinkten Wikipedia-Artikel liest. Einfach ausgedrückt ermöglicht die MathKlassenspezifikation die Verwendung plattformspezifischer Algorithmen, die nicht unbedingt das gleiche Ergebnis wie andere Plattformen liefern.
Coobird
1
@Aivar ist es nicht nur Präzision vs. Effizienz, sondern auch, dass es in vielen Fällen nicht unbedingt eine offensichtliche "genaueste Antwort" gibt. Zum Beispiel enthält der Sinus-Artikel Coobird-Links zu Erwähnungen "Es gibt keinen Standardalgorithmus zur Berechnung des Sinus".
dimo414
19

@ntoskrnl Als jemand, der mit JVM-Interna arbeitet, möchte ich Ihre Meinung unterstützen, dass "Intrinsics sich nicht unbedingt so verhalten wie StrictMath-Methoden". Um dies herauszufinden (oder zu beweisen), können wir einfach einen einfachen Test schreiben.

Wenn wir Math.powzum Beispiel den Java-Code für java.lang.Math.pow (double a, double b) untersuchen, werden wir sehen:

 public static double pow(double a, double b) {
    return StrictMath.pow(a, b); // default impl. delegates to StrictMath
}

Es steht der JVM jedoch frei, sie mit Eigen- oder Laufzeitaufrufen zu implementieren. Daher kann das zurückkehrende Ergebnis von dem abweichen, was wir erwarten würden StrictMath.pow.

Und der folgende Code zeigt diesen Aufruf Math.pow()gegenStrictMath.pow()

//Strict.java, testing StrictMath.pow against Math.pow
import java.util.Random;
public class Strict {
    static double testIt(double x, double y) {
        return Math.pow(x, y);
    }
    public static void main(String[] args) throws Exception{
        final double[] vs = new double[100];
        final double[] xs = new double[100];
        final double[] ys = new double[100];
        final Random random = new Random();

        // compute StrictMath.pow results;
        for (int i = 0; i<100; i++) {
            xs[i] = random.nextDouble();
            ys[i] = random.nextDouble();
            vs[i] = StrictMath.pow(xs[i], ys[i]);
        }
        boolean printed_compiled = false;
        boolean ever_diff = false;
        long len = 1000000;
        long start;
        long elapsed;
        while (true) {
            start = System.currentTimeMillis();
            double blackhole = 0;
            for (int i = 0; i < len; i++) {
                int idx = i % 100;
                double res = testIt(xs[idx], ys[idx]);
                if (i >= 0 && i<100) {
                    //presumably interpreted
                    if (vs[idx] != res && (!Double.isNaN(res) || !Double.isNaN(vs[idx]))) {
                        System.out.println(idx + ":\tInterpreted:" + xs[idx] + "^" + ys[idx] + "=" + res);
                        System.out.println(idx + ":\tStrict pow : " + xs[idx] + "^" + ys[idx] + "=" + vs[idx] + "\n");
                    }
                }
                if (i >= 250000 && i<250100 && !printed_compiled) {
                    //presumably compiled at this time
                    if (vs[idx] != res && (!Double.isNaN(res) || !Double.isNaN(vs[idx]))) {
                        System.out.println(idx + ":\tcompiled   :" + xs[idx] + "^" + ys[idx] + "=" + res);
                        System.out.println(idx + ":\tStrict pow :" + xs[idx] + "^" + ys[idx] + "=" + vs[idx] + "\n");
                        ever_diff = true;
                    }
                }
            }
            elapsed = System.currentTimeMillis() - start;
            System.out.println(elapsed + " ms ");
            if (!printed_compiled && ever_diff) {
                printed_compiled = true;
                return;
            }

        }
    }
}

Ich habe diesen Test mit OpenJDK 8u5-b31 durchgeführt und das folgende Ergebnis erhalten:

10: Interpreted:0.1845936372497491^0.01608930867480518=0.9731817015518033
10: Strict pow : 0.1845936372497491^0.01608930867480518=0.9731817015518032

41: Interpreted:0.7281259501809544^0.9414406865385655=0.7417808233050295
41: Strict pow : 0.7281259501809544^0.9414406865385655=0.7417808233050294

49: Interpreted:0.0727813262968815^0.09866028976654662=0.7721942440239148
49: Strict pow : 0.0727813262968815^0.09866028976654662=0.7721942440239149

70: Interpreted:0.6574309575966407^0.759887845481148=0.7270872740201638
70: Strict pow : 0.6574309575966407^0.759887845481148=0.7270872740201637

82: Interpreted:0.08662340816125613^0.4216580281197062=0.3564883826345057
82: Strict pow : 0.08662340816125613^0.4216580281197062=0.3564883826345058

92: Interpreted:0.20224488115245098^0.7158182878844233=0.31851834311978916
92: Strict pow : 0.20224488115245098^0.7158182878844233=0.3185183431197892

10: compiled   :0.1845936372497491^0.01608930867480518=0.9731817015518033
10: Strict pow :0.1845936372497491^0.01608930867480518=0.9731817015518032

41: compiled   :0.7281259501809544^0.9414406865385655=0.7417808233050295
41: Strict pow :0.7281259501809544^0.9414406865385655=0.7417808233050294

49: compiled   :0.0727813262968815^0.09866028976654662=0.7721942440239148
49: Strict pow :0.0727813262968815^0.09866028976654662=0.7721942440239149

70: compiled   :0.6574309575966407^0.759887845481148=0.7270872740201638
70: Strict pow :0.6574309575966407^0.759887845481148=0.7270872740201637

82: compiled   :0.08662340816125613^0.4216580281197062=0.3564883826345057
82: Strict pow :0.08662340816125613^0.4216580281197062=0.3564883826345058

92: compiled   :0.20224488115245098^0.7158182878844233=0.31851834311978916
92: Strict pow :0.20224488115245098^0.7158182878844233=0.3185183431197892

290 ms 

Bitte beachten Sie, dass dies Randomzum Generieren der x- und y-Werte verwendet wird, sodass Ihr Kilometerstand von Lauf zu Lauf variiert. Aber eine gute Nachricht ist, dass zumindest die Ergebnisse der kompilierten Version von Math.powdenen der interpretierten Version von übereinstimmen Math.pow. (Off Topic: Auch diese Konsistenz wurde erst 2012 mit einer Reihe von Bugfixes von OpenJDK-Seite durchgesetzt.)

Der Grund?

Das liegt daran, dass OpenJDK zur Implementierung Math.pow(und anderer mathematischer Funktionen) intrinsische und Laufzeitfunktionen verwendet , anstatt nur den Java-Code auszuführen. Der Hauptzweck besteht darin, x87-Anweisungen zu nutzen, damit die Leistung für die Berechnung gesteigert werden kann. Daher StrictMath.powwird Math.powzur Laufzeit nie von aufgerufen (für die OpenJDK-Version, die wir gerade verwendet haben, um genau zu sein).

Und diese Anordnung ist laut dem Javadoc der MathKlasse (ebenfalls von @coobird oben zitiert) völlig legitim :

Die Klasse Math enthält Methoden zum Ausführen grundlegender numerischer Operationen wie der elementaren Exponential-, Logarithmus-, Quadratwurzel- und trigonometrischen Funktionen.

Im Gegensatz zu einigen numerischen Methoden der Klasse StrictMath sind nicht alle Implementierungen der entsprechenden Funktionen der Klasse Math so definiert, dass sie bitweise dieselben Ergebnisse zurückgeben. Diese Entspannung ermöglicht leistungsfähigere Implementierungen, bei denen keine strikte Reproduzierbarkeit erforderlich ist.

Standardmäßig rufen viele der Math-Methoden einfach die entsprechende Methode in StrictMath für ihre Implementierung auf. Codegeneratoren werden aufgefordert, plattformspezifische native Bibliotheken oder Mikroprozessoranweisungen zu verwenden, sofern verfügbar, um leistungsfähigere Implementierungen von mathematischen Methoden bereitzustellen. Solche leistungsstärkeren Implementierungen müssen weiterhin der Spezifikation für Mathematik entsprechen.

Und die Schlussfolgerung? Stellen Sie für Sprachen mit dynamischer Codegenerierung wie Java sicher, dass das, was Sie aus dem 'statischen' Code sehen, mit dem übereinstimmt, was zur Laufzeit ausgeführt wird. Ihre Augen können Sie manchmal wirklich irreführen.

Tony Guan
quelle
18

Haben Sie den Quellcode überprüft? Viele Methoden in java.lang.Mathwerden an delegiert java.lang.StrictMath.

Beispiel:

public static double cos(double a) {
    return StrictMath.cos(a); // default impl. delegates to StrictMath
}
z00bs
quelle
18
+1 zum Lesen des Java-Quellcodes. Dies ist eine Stärke von Java über .NET: Ein großer Teil des Quellcodes für die Java-API wird mit dem JDK in einer Datei namens src.zip geliefert. Und was nicht da ist, kann jetzt heruntergeladen werden, da die JVM Open Source ist. Das Lesen der Java-Quelle ist möglicherweise nicht die am meisten beworbene Methode zur Lösung von Problemen: Es scheint eine schlechte Idee zu sein, da Sie normalerweise "die öffentliche Schnittstelle und nicht die Implementierung einhalten" sollen. Das Lesen der Quelle hat jedoch einen großen Vorteil: Es gibt Ihnen immer die Wahrheit. Und manchmal ist das das Wertvollste von allen.
Mike Clark
1
@ Andrew Danke für den Tipp. Ich habe gerade ein Tutorial gelesen, wie man das in Visual Studio einrichtet. Java kann immer noch einen kleinen Vorteil haben, da Sie den Quellcode für die VM selbst herunterladen können, nicht nur die Standardbibliothek (Framework). Trotzdem danke!
Mike Clark
12
Leider sagt der Quellcode in diesem Fall nicht die ganze Wahrheit. Die JVM kann die Methoden in Math durch plattformspezifische Eigenheiten ersetzen. Intrinsics verhalten sich nicht unbedingt wie StrictMath-Methoden, ihr Verhalten wird jedoch durch die Dokumentation in der Math-Klasse eingeschränkt.
ntoskrnl
3
Schlüsselwort gibt es "Standard" - auf vielen Plattformen Mathwird nicht wirklich verwendet StrictMath.
dimo414
1
Dies ist zwar eine interessante Beobachtung, beantwortet aber nicht die Frage.
Carl G
0

Zitieren von java.lang.Math :

Die Genauigkeit der Gleitkommamethoden Mathwird in ulps , Einheiten an letzter Stelle, gemessen .

...

Wenn eine Methode immer einen Fehler von weniger als 0,5 ulps aufweist, gibt die Methode immer die Gleitkommazahl zurück, die dem genauen Ergebnis am nächsten liegt. Eine solche Methode ist korrekt gerundet . Eine korrekt gerundete Methode ist im Allgemeinen die beste, die eine Gleitkomma-Näherung sein kann. Es ist jedoch unpraktisch, dass viele Gleitkomma-Methoden korrekt gerundet werden.

Und dann sehen wir unter Math.pow (..) zum Beispiel:

Das berechnete Ergebnis muss innerhalb von 1 ulp des genauen Ergebnisses liegen.

Was ist nun der Ulp? Wie erwartet java.lang.Math.ulp(1.0)ergibt sich 2.220446049250313e-16, was 2 -52 ist . ( Math.ulp(8)Gibt auch den gleichen Wert wie Math.ulp(10)und an Math.ulp(15), aber nicht Math.ulp(16).) Mit anderen Worten, wir sprechen über das letzte Stück der Mantisse.

Das von zurückgegebene Ergebnis java.lang.Math.pow(..)kann also im letzten der 52 Bits der Mantisse falsch sein, wie wir in Tony Guans Antwort bestätigen können.

Es wäre schön, einen konkreten 1-ulp- und 0,5-ulp-Code zum Vergleich auszugraben. Ich werde spekulieren, dass ziemlich viel zusätzliche Arbeit erforderlich ist, um das letzte Bit korrekt zu machen, aus dem gleichen Grund, aus dem wir, wenn wir zwei Zahlen A und B kennen, auf 52 signifikante Zahlen gerundet sind und A × B auf 52 signifikante Zahlen korrigieren möchten Bei korrekter Rundung müssen wir tatsächlich einige zusätzliche Bits von A und B kennen, um das letzte Bit von A × B richtig zu machen. Das heißt aber, wir sollten die Zwischenergebnisse A und B nicht runden, indem wir sie zu Doppel erzwingen . Wir benötigen effektiv einen breiteren Typ für Zwischenergebnisse. (Wie ich gesehen habe, beruhen die meisten Implementierungen mathematischer Funktionen stark auf Multiplikationen mit hartcodierten vorberechneten Koeffizienten. Wenn sie also breiter als doppelt sein müssen, gibt es einen großen Effizienzverlust.)

Evgeni Sergeev
quelle