Ich habe eine Funktion, die einige mathematische Berechnungen durchführt und ein double zurückgibt. Unter Windows und Android führt dies zu unterschiedlichen Ergebnissen, da die std::exp - Implementierung unterschiedlich ist (Warum erhalte ich ein plattformspezifisches Ergebnis für std :: exp?). Der e-17-Rundungsunterschied wird propagiert und am Ende ist es nicht nur ein Rundungsunterschied, den ich bekomme (die Ergebnisse können sich am Ende zwischen 2,36 und 2,47 ändern). Wenn ich das Ergebnis mit einigen erwarteten Werten vergleiche, möchte ich, dass diese Funktion auf allen Plattformen das gleiche Ergebnis zurückgibt.

Also muss ich mein Ergebnis abrunden. Die einfachste Lösung, dies zu tun, ist anscheinend (soweit ich im Web finden konnte) std::ceil(d*std::pow<double>(10,precision))/std::pow<double>(10,precision). Ich bin jedoch der Meinung, dass dies je nach Plattform immer noch zu unterschiedlichen Ergebnissen führen kann (und außerdem ist es schwierig zu entscheiden, was precision sein soll).

Ich habe mich gefragt, ob das Hardcodieren des niedrigstwertigen Bytes von double eine gute Rundungsstrategie sein könnte.

Dieser Schnelltest scheint zu zeigen, dass "Ja":

#include <iostream>
#include <iomanip>

double roundByCast( double d )
{
    double rounded = d;
    unsigned char* temp = (unsigned char*) &rounded;
    // changing least significant byte to be always the same
    temp[0] = 128;
    return rounded;
}

void showRoundInfo( double d, double rounded )
{
    double diff = std::abs(d-rounded);
    std::cout << "cast: " << d << " rounded to " << rounded << " (diff=" << diff << ")" << std::endl;
}

void roundIt( double d )
{
    showRoundInfo( d, roundByCast(d) );
}

int main( int argc, char* argv[] )
{
    roundIt( 7.87234042553191493141184764681 );
    roundIt( 0.000000000000000000000184764681 );
    roundIt( 78723404.2553191493141184764681 );
}

Dies gibt aus:

cast: 7.87234 rounded to 7.87234 (diff=2.66454e-14)
cast: 1.84765e-22 rounded to 1.84765e-22 (diff=9.87415e-37)
cast: 7.87234e+07 rounded to 7.87234e+07 (diff=4.47035e-07)

Meine Frage ist:

  • Ist unsigned char* temp = (unsigned char*) &rounded sicher oder gibt es hier ein undefiniertes Verhalten und warum?
  • Wenn es keine UB gibt (oder wenn es einen besseren Weg gibt, dies ohne UB zu tun), ist eine solche Rundungsfunktion für alle Eingaben sicher und genau?

Hinweis: Ich weiß, dass Gleitkommazahlen ungenau sind. Bitte nicht als Duplikat von markieren. Ist die Gleitkomma-Mathematik fehlerhaft? oder Warum sind Gleitkommazahlen ungenau?. Ich verstehe, warum die Ergebnisse unterschiedlich sind. Ich suche nur nach einer Möglichkeit, sie auf allen Zielplattformen identisch zu machen.


Bearbeiten, ich kann meine Frage neu formulieren, wenn Leute fragen, warum ich unterschiedliche Werte habe und warum ich möchte, dass sie gleich sind.

Angenommen, Sie erhalten ein double aus einer Berechnung, die aufgrund plattformspezifischer Implementierungen (wie std::exp) einen anderen Wert haben könnte. Wenn Sie diese unterschiedlichen double korrigieren möchten, um auf allen Plattformen genau dieselbe Speicherdarstellung (1) zu erhalten, und die geringstmögliche Genauigkeit verlieren möchten, ist es ein guter Ansatz, das niedrigstwertige Byte zu reparieren ? (weil ich der Meinung bin, dass das Runden auf eine beliebige Genauigkeit wahrscheinlich mehr Informationen verliert als dieser Trick).

(1) Mit "gleicher Darstellung" meine ich, dass Sie, wenn Sie es in ein std::bitset umwandeln, für alle Plattformen die gleiche Bitfolge sehen möchten.

0
jpo38 18 Jän. 2019 im 17:55

4 Antworten

Beste Antwort

Ist unsigned char * temp = (unsigned char *) & round safe oder gibt es hier ein undefiniertes Verhalten und warum?

Es ist gut definiert, da Aliasing durch unsigned char zulässig ist .

Ist eine solche Rundungsfunktion für alle Eingaben sicher und genau?

Nein. Sie können dieses Problem mit Abschneiden / Runden nicht perfekt beheben. Bedenken Sie, dass eine Implementierung 0x.....0ff und die andere 0x.....100 ergibt. Wenn Sie den lsb auf 0x00 setzen, ergibt sich ein ursprünglicher Unterschied von 1 ulp zu 256 ulps.

Kein Rundungsalgorithmus kann dies beheben.

Sie haben zwei Möglichkeiten:

  • Verwenden Sie kein Gleitkomma, sondern eine andere Methode (z. B. Festkomma).
  • Betten Sie eine Gleitkomma-Bibliothek in Ihre Anwendung ein, die nur grundlegende Gleitkomma-Arithmetik (+, -, *, /, sqrt) verwendet, und verwenden Sie nicht -ffast-math oder eine gleichwertige Option. Auf diese Weise sollten die Gleitkommaergebnisse auf einer IEEE-754-kompatiblen Plattform dieselben sein, da IEEE-754 vorschreibt, dass grundlegende Operationen "perfekt" berechnet werden müssen. Dies bedeutet, dass die Operation mit unendlicher Genauigkeit berechnet und dann auf die resultierende Darstellung gerundet wird.

Übrigens, wenn eine Eingabedifferenz 1e-17 eine große Ausgabedifferenz bedeutet, ist Ihr Problem / Algorithmus schlecht konditioniert, was generell vermieden werden sollte, da es normalerweise keine aussagekräftigen Ergebnisse liefert.

3
geza 18 Jän. 2019 im 23:08

Nein, Rundung ist keine Strategie, um kleine Fehler zu beseitigen oder die Übereinstimmung mit fehlerhaften Berechnungen zu gewährleisten.

Wenn Sie die Zahlenlinie in Bereiche aufteilen, werden Sie die geringsten Abweichungen erfolgreich beseitigen (indem Sie sie in denselben Eimer legen und auf denselben Wert klemmen). Sie erhöhen jedoch die Abweichung erheblich, wenn Ihr ursprüngliches Wertepaar eine Grenze überspannt.

In Ihrem speziellen Fall der Hardcodierung des niedrigstwertigen Bytes die sehr nahen Werte

0x1.mmmmmmm100

Und

0x1.mmmmmmm0ff

Haben eine Abweichung von nur einem ULP ... aber nach Ihrer Rundung unterscheiden sie sich um 256 ULP. Hoppla!

5
Ben Voigt 18 Jän. 2019 im 20:42

Was Sie tun, ist völlig fehlgeleitet.

Ihr Problem ist nicht, dass Sie unterschiedliche Ergebnisse erhalten (2,36 vs. 2,47). Ihr Problem ist, dass mindestens eines dieser Ergebnisse und wahrscheinlich beide massive Fehler aufweisen. Ihre Windows- und Android-Ergebnisse unterscheiden sich nicht nur, sie sind FALSCH. (Mindestens einer von ihnen, und Sie haben keine Ahnung, welcher).

Finden Sie heraus, warum Sie diese massiven Fehler erhalten, und ändern Sie Ihre Algorithmen, um winzige Rundungsfehler nicht massiv zu erhöhen. Oder Sie haben ein Problem, das von Natur aus chaotisch ist. In diesem Fall ist der Unterschied zwischen den Ergebnissen tatsächlich eine sehr nützliche Information.

Was Sie versuchen, vergrößert die Rundungsfehler nur um das 256-fache. Wenn zwei unterschiedliche Ergebnisse zu .... 1ff und .... 200 hexadezimal enden, ändern Sie diese in .... 180 und .... 280, So kann auch der Unterschied zwischen leicht unterschiedlichen Zahlen um den Faktor 256 zunehmen.

Und auf einer Bigendian-Maschine wird Ihr Code einfach kaboom !!!

2
gnasher729 18 Jän. 2019 im 23:51

Ihre Funktion funktioniert aufgrund von Aliasing nicht.

double roundByCast( double d )
{
    double rounded = d;
    unsigned char* temp = (unsigned char*) &rounded;
    // changing least significant byte to be always the same
    temp[0] = 128;
    return rounded;
}

Das Umwandeln in nicht signiertes char * für temp ist zulässig, da char * -Cast die Ausnahme von den Aliasing-Regeln darstellt. Dies ist für Funktionen wie Lesen, Schreiben, Speichern usw. erforderlich, damit sie Werte in und aus Byte-Darstellungen kopieren können.

Sie dürfen jedoch nicht in temp [0] schreiben und dann davon ausgehen, dass sich die Rundung geändert hat. Sie müssen eine neue Doppelvariable erstellen (auf dem Stapel ist in Ordnung) und memcpy temp darauf zurücksetzen.

0
Zan Lynx 18 Jän. 2019 im 20:58