Gleitkommaarithmetik

Aus RAD Studio
Wechseln zu: Navigation, Suche

Nach oben zu Routinen für Gleitkommawerte

Für die Arbeit mit Gleitkommazahlen ist die Kenntnis der internen Darstellung von Daten unerlässlich. Programmierer müssen die Probleme bei der endlichen Genauigkeit kennen:

  • Bei integralen Werten (Integertypen) müssen Sie die Möglichkeit eines Überlaufs in Betracht ziehen.
  • Bei Gleitkommawerten (mit einfacher, doppelter oder "extended" Genauigkeit) müssen Sie die Möglichkeit eines Genauigkeitsverlusts berücksichtigen.

Auswirkungen der endlichen Genauigkeit

Der Genauigkeitsverlust von Gleitkommawerten kann vielfältige Ursachen haben:

  • Wenn Sie einer Gleitkommavariable ein Gleitkommaliteral (z. B.: 0,1) zuweisen, könnte die Gleitkommavariable nicht genügend Bits zur fehlerfreien Aufnahme des gewünschten Wertes enthalten. Für das Gleitkommaliteral könnte eine größere Anzahl oder sogar eine unendliche Anzahl von Bits zur Darstellung einer unendlichen Genauigkeit erforderlich sein.


uses
  System.SysUtils;
var
  X: Single;
  Y: Double;
begin
  X := 0.1;
  Y := 0.1;
  Writeln('X =', X);
  Writeln('Y =', Y);
  Writeln(Format('Exponent X: %x', [X.Exp]));
  Writeln(Format('Mantissa Y: %x', [Y.Mantissa]));
  ReadLn;
end.
Konsolenausgabe:
X = 1.00000001490116E-0001
Y = 1.00000000000000E-0001
Exponent Y: 3FB
Mantissa Y: 1999999999999A
Im obigen Code befindet sich der Fehler in der neunten Ziffer der Darstellung der einfachen (Single) Genauigkeit. Die Darstellung der doppelten (Double) Genauigkeit enthält diesen Fehler ebenfalls. Zur Demonstration dieses Fehlers wird der Roh-Exponent und die Mantisse (Mantissa) der Zahl mit der doppelten (Double) Genauigkeit verwendet. Der hexadezimale Exponent $3FB hat die dezimale Darstellung 1019. Da die interne Darstellung von Double-Zahlen ein Bias von 1023 hat, gilt Exponent = 1019 - 1023 = -4. Die hexadezimale Mantisse $1999999999999A hat die binäre Darstellung 11001100110011001100110011001100110011001100110011010. Daher ist die binäre Darstellung von Y 1.1001100110011001100110011001100110011001100110011010*2-4 oder 0.00011001100110011001100110011001100110011001100110011010*2-4. Diese Zahl ist ein Näherungswert der doppelten Genauigkeit. Die genaue Zahl 0.1 wird als sich unendlich wiederholender Bruch dargestellt 0.0(0011).
Der maximale Fehler, der auf diese Weise produziert werden kann, ist jedoch nur 0,5 ULP (EN).
  • Bei Gleitkommaoperationen kann jeder Schritt (jede Operation) einen speziellen Fehler hervorrufen. Und zwar deshalb, weil bei einigen Operationen das berechnete Ergebnis nicht mit der vollständigen Genauigkeit gespeichert werden kann. Wenn Sie beispielsweise zwei Zahlen multiplizieren, S1-Bits mit S2-Bits (gilt für integrale und Gleitkommatypen), dann erfordert das Ergebnis für eine vollständige Genauigkeit S1-Bits + S2-Bits.
Die "Menge" der von einer Operation verursachten Fehler hängt vom Prozessormodell und der Art der Operation ab. Additive Operationen produzieren relativ wenig Fehler, Multiplikationen relativ viele.

Der Genauigkeitsverlust (Fehler) bei Gleitkommaoperationen wird von Berechnung zu Berechnung weitergegeben. Es ist Aufgabe des Programmierers, einen Algorithmus zu entwickeln, der dennoch korrekt ist.

Eine Gleitkommavariable kann als Integervariable mit einer Zweierpotenzskala angesehen werden. Wenn Sie der Gleitkommavariable einen Extremwert "aufzwingen", wird die Skala automatisch angepasst. Deshalb könnte der Anschein erweckt werden, dass die Gleitkommavariable nicht überlaufen kann. Und das ist tatsächlich so, aber es gibt andere Risiken: ein Fehler einer Gleitkommavariable kann sich zu einem signifikanten Fehler akkumulieren, und/oder eine Gleitkommavariable kann denormalisiert werden Denormal number (EN).

Verwendung größerer Datentypen

Der einfachste Weg zur Lösung des Problems des Integerüberlaufs oder des Genauigkeitsverlusts bei Gleitkommazahlen (im Allgemeinen Auswirkungen der endlichen Genauigkeit) besteht in der Verwendung von Datentypen aus derselben Klasse (Integral oder Gleitkomma), aber mit größerer Kapazität. Wenn z. B. ein ShortInt-Wert überläuft, können Sie problemlos zu einem LongInt-, FixedInt- oder Int64-Wert wechseln. Genauso können Sie, wenn ein Gleitkommawert mit einfacher Genauigkeit zu ungenau ist, einen Gleitkommawert mit doppelter Genauigkeit verwenden. Dabei müssen Sie aber Folgendes berücksichtigen:

  • Der Datentyp mit der größeren Speicherkapazität könnte trotzdem nicht ausreichen.
  • Für den Datentyp mit der größeren Speicherkapazität ist mehr Arbeitsspeicher und bei Operationen sind möglicherweise mehrere CPU-Zyklen erforderlich.

Steuereinstellungen

Auf der 32-Bit-Plattform werden dem x87-FPU-Steuerwort (CW) zwei Bit zur Festlegung des Rundungsmodus zugewiesen. Siehe Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 1: Basic Architecture > 8.1.5.3 Rounding Control Field (EN). Bei 64-Bit-Programmen wird der Rundungsmodus vom SSE-Steuerregister, MSXCSR, festgelegt. Mit System.Math.SetRoundMode können Sie den Rundungsmodus ändern.

Der FPU-Rundungsmodus kann Auswirkungen auf RTL-Funktionen haben, die mit Gleitkommavariablen arbeiten. Die genaue Art der Ergebnisänderungen von auf dem FPU-Steuerwort basierten RTL-Routinen hängt von dem implementierten Algorithmus ab. Die Rundung wirkt sich auf alle Operationen aus, deren Ergebnisse ohne Rundung nicht in den Zieltyp passen würden (bei Gleitkommamultiplikationen ist beispielsweise fast immer eine Rundung beteiligt). Wenn eine Funktion aus vielen Gleitkommamultiplikationen besteht, hat der Rundungsmodus große Auswirkungen.

Mithilfe des Rundungsmodus kann auch eine Intervallarithmetik implementiert werden: Verkürzt dargestellt, wird dabei derselbe Algorithmus zuerst mit einem Aufrundungsmodus und dann mit einem Abrundungsmodus ausgeführt und schließlich die Differenz der beiden Ergebnisse betrachtet. Dies vermittelt eine Vorstellung des potenziellen durch das Runden eingeführten Fehlers und der potenziellen Ungenauigkeit.

Anwendungsfälle

Finanzmathematische Berechnungen

IEEE-Gleitkommawerte eignen sich nur bedingt für finanzmathematische Berechnungen. Und zwar, weil für derartige Berechnungen äußerst strenge Anforderungen an die Genauigkeit gestellt werden. Sie sollten stattdessen die Verwendung von integralen Typen (primitive Integertypen oder Currency) oder BCD-Typen in Erwägung ziehen.

Die Unit Data.FmtBcd enthält die Unterstützung für BCD-Operationen. Das BCD-Format hat das folgende wichtige Merkmal: jede Dezimalziffer (Basis 10 Ziffern) wird mit 4 Bit Arbeitsspeicher (ein Nibble, Halbbyte) codiert.

Im folgenden Code wird die Verwendung eines TBcd-Wertes als Variante gezeigt:

Delphi:

var
  X: Variant;

begin
  X := VarFMTBcdCreate('0.1', 16 { Precision }, 2 { Scale });
  Writeln(VarToStr(X));

  // ...

C++:

#include <Variants.hpp>
#include <FMTBcd.hpp>

int _tmain(int argc, _TCHAR* argv[]) {
  Variant x = VarFMTBcdCreate("0.1", 16 /* Precision */, 2 /* Scale */);
  printf("%ls", VarToStr(x).c_str());

 // ...

Konsolenausgabe:

0.1

Der obige Code zeigt, dass mit einer BCD-Variable die Konvertierung von Text in das numerische Format fehlerlos durchgeführt werden kann.

Für finanzmathematische Berechnungen eignet sich der Typ Currency. Der Typ Currency ist im Wesentlichen eine mit dem Faktor 10.000 skalierte Ganzzahl (dieser Wert ermöglicht die exakte Division durch 10). Vier Dezimalziffern können in einer Currency-Variable gespeichert werden, alles, was darüber hinausgeht, wird gerundet.

Delphi:

var
  X, Y: Currency;

begin
  X := 0.1;
  Y := 0.00001;

  Writeln(X);
  Writeln(Y);

  // ...

C++:

#include <System.hpp>

int _tmain(int argc, _TCHAR* argv[]) {
  Currency x, y;
  x = 0.1;
  y = 0.00001;

  printf("%2.14E\n", (double)x);
  printf("%2.14E\n", (double)y);

  // ...

Konsolenausgabe:

1.00000000000000E-0001
0.00000000000000E+0000

Die C++-Implementierung von Currency ist in $BDS\include\rtl\syscurr.h enthalten.

Das Currency-Intervall umfasst [-922337203685477,5807; 922337203685477,5807].

Physikalische (wissenschaftliche) Berechnungen/Simulationen

Für wissenschaftliche Berechnungen sind im Allgemeinen umfangreiche Berechnungen erforderlich, und eine Vergrößerung der Gleitkommagenauigkeit könnte nicht erwünscht sein. Operationen mit der Genauigkeit "Extended" werden weniger unterstützt (siehe Delphi für x64).

Bei der Verwendung von SSE müssen Sie bedenken, dass ein SSE-Register zwei Variablen mit doppelter oder vier Variablen mit einfacher Genauigkeit aufnehmen kann. Deshalb können Sie gleichzeitig mehr Operationen mit einfacher Genauigkeit als mit doppelter Genauigkeit durchführen.

Ein interessantes und nützliches Vorgehen ist das folgende: Verwenden Sie Gleitkommatypen mit einfacher Genauigkeit, aber reduzieren Sie den akkumulierten Fehler in regelmäßigen Abständen. In vielen Anwendungen kann ein geringer Genauigkeitsverlust toleriert werden; es ist nur wichtig, die Abweichung irgendwie aufzuheben. Ein Beispiel einer derartigen Implementierung ist die räumliche 3D-Drehung mit Quaternionen. Siehe Physically Based Modeling > Rigid Body Dynamics (ONLINE SIGGRAPH 2001 COURSE NOTES) (EN).

Digitale Signalverarbeitung

Alle DSP-Variablen sind im Allgemeinen mit Fehlern "kontaminiert". Sie sollten deshalb den optimalen Kompromiss zwischen Datengenauigkeit und Rechenaufwand abwägen.

Allgemeine Schlussfolgerungen

"Was Sie sehen, ist nicht das, was Sie bekommen"

Im Quellcode mit Dezimalziffern enthaltene und auf dem Bildschirm angezeigte Gleitkommazahlen unterscheiden sich wahrscheinlich von deren Repräsentation im Arbeitsspeicher. Gehen Sie nicht davon aus, dass das, was im Konsolenfenster angezeigt wird, exakt dem entspricht, was im Arbeitsspeicher vorhanden ist. Konvertierungen von Dezimal- in Binärwerte (und zurück) können nicht in jedem Fall fehlerfrei durchgeführt werden.

Verwenden Sie zur Vermeidung des IEEE-Repräsentationsfehlers von Gleitkommawerten integrale, BCD- oder Currency-Variablen.

Grundlagen des Datenflusses

In Delphi werden auf x86 Zwischenergebnisse von Gleitkommaausdrücken mit einfacher (Single) Genauigkeit immer als Extended gespeichert.

Standardmäßig wird für alle arithmetischen x64-Operationen und -Ausdrücke, die nur Gleitkommawerte mit einfacher (Single) Genauigkeit betreffen, eine hohe Genauigkeit durch Speichern der Zwischenergebnisse als Gleitkommawerte mit doppelter (Double) Genauigkeit beibehalten. Diese Operationen sind daher langsamer als mit Operanden mit expliziter doppelter Genauigkeit (der compilierte Code konvertiert bei jeder Operation Single-Werte in Double-Werte). Wenn die Ausführungsgeschwindigkeit wesentlich ist, kennzeichnen Sie den fraglichen Code zur Deaktivierung von Zwischenwerten mit doppelter Genauigkeit mit der Direktive {$EXCESSPRECISION OFF}. Ansonsten wird die Standarddirektive ({$EXCESSPRECISION ON}) zur Erhöhung der Genauigkeit des Ergebniswertes empfohlen. Die Direktive {$EXCESSPRECISION OFF} wirkt sich nur auf x64-Ziel-CPUs aus.

In C++ kann ein Gleitkommaliteral einen Gleitkommawert entweder mit einfacher oder mit doppelter Genauigkeit repräsentieren: dies ist vom Suffix f abhängig. Wenn f in C++ an ein Gleitkommaliteral angehängt wird, dann verwendet der Compiler die einfache Genauigkeit. Grundlagen zur Genauigkeit von Zwischenwerten finden Sie in den veröffentlichten ISO-Standards.

Gleitkommaoperationen sind eventuell nicht verknüpft

Aufgrund des von allen Operatoren erzeugten Fehlers ist die Ausführungsreihenfolge in Berechnungen wichtig.

Siehe CERT C Secure Coding Standards, Recommendation FLP01-C (EN).

Gleitkomma-Exceptions

Gleitkommaoperationen können zu verschiedenen fehlerhaften Situationen, wie Gleitkommaüberlauf, Division durch Null, denormalisierter Wert, Generierung von NaN-Werten und Ausführung anderer ungültiger Gleitkommaoperationen, führen. Normalerweise führen solche Situationen zu Gleitkomma-Exceptions. Die Unit System.Math enthält:

Die jeweilige Gleitkomma-Hardware stellt bestimmte Mittel zum Steuern der Gleitkomma-Exceptions bereit:

  • Auf Intel 32-Bit-Prozessoren ist dies das FPU-Steuerwort.
  • Auf Intel 64-Bit-Prozessoren ist dies das SSE-Steuerwort.
  • Wir unterstützen keine Gleitkomma-Exceptions auf der ARM-Architektur. Daher werden alle Gleitkomma-Exceptions auf der ARM-Architektur maskiert.

Siehe auch