Dezimale Gleitkommazahlen (_Decimal32, _Decimal64, _Decimal128)
Wer mit binären Gleitkommazahlen rechnet, stößt schnell auf Rundungsfehler. Werte wie 0.1 oder 0.2 lassen sich im Binärsystem nicht exakt darstellen, sodass sich bei wiederholten Rechnungen kleine Fehler summieren. Der Grund liegt darin, dass Brüche wie 1/10 im Binärsystem eine unendliche periodische Darstellung besitzen, ganz ähnlich wie 1/3 im Dezimalsystem. Da IEEE 754 nur endlich viele Bits speichern kann, muss diese Periode abgeschnitten und gerundet werden. Das führt dazu, dass tatsächlich nicht exakt 0.1, sondern nur ein sehr naher Näherungswert gespeichert wird.
Um dieses Problem zu vermeiden, gibt es drei optionale, dezimalbasierte Datentypen (seit C23), die aus der Technical Specification 18661-2 (TS 18661) übernommen wurden:
| Typ | Speicherbreite | Genauigkeit (Dezimalstellen) | Literalsuffix |
|---|---|---|---|
_Decimal32 |
32 Bit | ca. 7 | df |
_Decimal64 |
64 Bit | ca. 16 | dd |
_Decimal128 |
128 Bit | ca. 34 | dl |
Wie float und double besitzen auch die dezimalen Gleitkommatypen eine endliche Präzision.
Intern bestehen sie aus denselben logischen Komponenten Vorzeichenbit, Exponent und Mantisse (Signifikand), unterscheiden sich aber in der Basis: Binäre Typen verwenden Basis 2, dezimale Typen Basis 10.
Damit sind dezimale Gleitkommazahlen den binären konzeptionell sehr ähnlich; sie speichern ebenfalls nur eine endliche Anzahl signifikanter Stellen. Auch hier werden Werte, die nicht exakt in die vorhandene Präzision passen, gerundet.
-
Binary float ist ›natürlich‹ exakt bei: Zweierpotenzen und Skalierung um 2^k^.
-
Decimal float ist ›natürlich‹ exakt bei: Dezimalzahlen (z. B.
0.1,123.45) und Skalierung um 10^k^.
Beide haben endliche Präzision und selbst in diesen ›natürlichen‹ Fällen ist die Darstellung nur dann exakt, wenn die erforderliche Anzahl signifikanter Stellen in das jeweilige Format passt.
Feature-Makros und Beispiel
Die dezimalen Typen entsprechen den IEEE-754-2008-Formaten für 32-, 64- und 128-Bit-Dezimalzahlen. Ihre Verfügbarkeit wird über das Makro __STDC_IEC_60559_DFP__ signalisiert. Ist es definiert, garantiert die Implementierung sowohl die Typen als auch die zugehörige Arithmetik und Bibliotheksfunktionen.
Wichtig: GCC kann die dezimalen Typen auch ohne dieses Makro bereitstellen (Front-End und Softwareemulation), jedoch ohne vollständige Bibliotheksunterstützung. In diesem Fall setzt GCC nur Compiler-Makros wie
__DEC32_MANT_DIG__,__DEC64_MANT_DIG__oder__DEC128_MANT_DIG__, nicht jedoch__STDC_IEC_60559_DFP__.
Ein Beispiel verdeutlicht den Unterschied zwischen binären und dezimalen Gleitkommazahlen:
#include <stdio.h>
int main() {
float binary_a = 0.1f;
float binary_b = 0.2f;
float binary_sum = binary_a + binary_b;
printf("Binäre Summe: %.10f\n", binary_sum); // 📣 0.3000000119
#if defined(__STDC_IEC_60559_DFP__) || defined(__DEC32_MANT_DIG__)
_Decimal64 decimal_a = 0.1dd;
_Decimal64 decimal_b = 0.2dd;
_Decimal64 decimal_sum = decimal_a + decimal_b;
printf("Dezimale Summe: %.10f\n", (double)decimal_sum);
// z. B. 📣 0.3000000000
#else
puts("Decimal Floating Types unterstützt dieser Compiler nicht.");
#endif
}
Wird ein Programm ohne Prüfung der Verfügbarkeit kompiliert, resultieren Fehler. Deshalb sollte man stets #if einsetzen.
(Quelle: ISO/IEC 9899:2024, § 6.10.8.3 Conditional feature macros)
Hat der Compiler die Unterstützung aktiviert, ergibt sich folgende Ausgabe:
Binäre Summe: 0.3000000119
Dezimale Summe: 0.3000000000
Die Ausgabe zeigt, dass dezimale Gleitkommazahlen exakte Ergebnisse liefern, während binäre nur Näherungswerte speichern. (Quelle: ISO/IEC 9899:2024, § 5.2.4.2.2 Characteristics of floatingtypes)
Typkompatibilität und Konvertierungen
Ein kritischer Aspekt bei der Verwendung dezimaler Gleitkommatypen ist deren strikte Trennung von den binären Gleitkommatypen:
-
Keine implizite Mischung: Dezimale Typen (
_Decimal32,_Decimal64,_Decimal128) und binäre Typen (float,double,long double) können nicht direkt in Ausdrücken gemischt werden. Das bedeutet etwa:float+ _Decimal64→ Compiler-Fehler
-
Ganzzahlige Typen: Anders verhält es sich mit ganzzahligen Datentypen wie
int,longoderunsigned int. Diese können ohne explizite Konvertierung mit dezimalen Gleitkommazahlen verrechnet werden. Das bedeutet etwa:int+_Decimal32→_Decimal32
Um Werte zwischen verschiedenen Typsystemen zu übertragen, müssen explizite Casts verwendet werden. Dies schützt vor unbeabsichtigten Präzisionsverlusten und macht Typkonvertierungen im Code sichtbar.
Ein Beispiel für notwendige Konvertierungen:
#include <stdio.h>
int main() {
#if defined(__STDC_IEC_60559_DFP__) || defined(__DEC32_MANT_DIG__)
_Decimal64 decimal_value = 1.5dd;
int integer_value = 3;
float binary_value = 2.0f;
// Direkte Mischung mit Ganzzahlen ist möglich
_Decimal64 result1 = decimal_value + integer_value;
// Fehler: Keine direkte Mischung möglich
// _Decimal64 result2 = decimal_value + binary_value; // Compiler-Fehler
// Korrekt: Explizite Konvertierung
_Decimal64 result3 = decimal_value + (_Decimal64)binary_value;
#else
puts("Decimal Floating Types unterstützt dieser Compiler nicht.");
#endif
}
Bleibt _Decimal64 result2 = decimal_value + binary_value; im Code, meldet GCC:
<source>:13:6: error: cannot mix operands of decimal floating and other ↩
floating types
13 | _Decimal64 result2 = decimal_value + binary_value;
| ^~~~~~~~~~
Diese strikte Typsicherheit mag zunächst umständlich erscheinen, verhindert aber subtile Fehler durch ungewollte Konvertierungen und macht den möglichen Informationsverlust bei der Umwandlung zwischen verschiedenen Zahlendarstellungen explizit.
Unterstützung durch Compiler und Bibliotheken
Unterstützt die Hardware keine nativen Dezimaltypen, kann der Compiler (z. B. GCC) diese in Software emulieren. Dafür werden interne Bibliotheksroutinen genutzt, die die arithmetischen Operatoren gemäß IEEE 754-2008 für decimal32, decimal64 und decimal128 implementieren. Im Fall von C übernehmen das die GCC Decimal-Float Library Routines (https://gcc.gnu.org/onlinedocs/gccint/Decimal-float-library-routines.html).
Damit dies funktioniert, muss GCC mit Unterstützung für Decimal Floating Point kompiliert worden sein (z. B. über die Option --enable-decimal-float). Nicht jede GCC-Build-Variante aktiviert diese Unterstützung standardmäßig.
Wichtig: Zwar stellt GCC die Compilerunterstützung für die Typen bereit, jedoch nicht automatisch die komplette C-Bibliotheksfunktionalität. Diese muss von einer separaten C-Bibliothek (z. B. libdfp) geliefert werden. Aus diesem Grund definiert GCC kein Makro __STDC_DEC_FP__, das eine vollständige DFP-Unterstützung signalisiert, da keine vollständige Konformität zur technischen Spezifikation garantiert ist. Siehe dazu auch https://gcc.gnu.org/onlinedocs/gcc/Decimal-Float.html.
Installation der Bibliotheksunterstützung
Für zusätzliche Funktionen, und damit Format-Strings funktionieren, wird die Laufzeitbibliothek libdfp (https://github.com/libdfp/libdfp) benötigt. Auf Debian/Ubuntu-basierten Systemen kann sie über die Paketverwaltung installiert werden:
$ sudo apt install libdfp-dev
Anschließend muss beim Kompilieren explizit gegen diese Bibliothek gelinkt werden:
$ cc program.c -ldfp
Ausgaben mit Format-Strings
Für Ein- und Ausgabe gibt es spezielle Literalsuffixe und zugehörige Formatangaben:
| Typ | Formatangabe (printf()/scanf()) |
|---|---|
_Decimal32 |
%Hf |
_Decimal64 |
%Df |
_Decimal128 |
%DDf |
Während wir vorher einen Cast eingesetzt haben, können wir nun direkt printf() mit einem Format-Platzhalter einsetzen:
#include <stdio.h>
int main() {
#if defined(__STDC_IEC_60559_DFP__) || defined(__DEC32_MANT_DIG__)
_Decimal64 a = 0.1dd;
_Decimal64 b = 0.2dd;
_Decimal64 sum = a + b;
printf("Dezimale Summe: %.10Df\n", sum); // Erwartet: ?? 0.3000000000
#else
puts("Decimal Floating Types unterstützt dieser Compiler nicht.");
#endif
}
Die Konvertierung in eine Zeichenkette übernimmt in dem Fall die Bibliothek, die mit -ldfp gelinkt werden muss. Nicht alle Umgebungen unterstützen diese Formatangaben zuverlässig.
Fallstricke
Die Nutzung optionaler Datentypen bringt einige Besonderheiten mit sich:
-
Portabilität: Da es sich um ein optionales Feature handelt, sollte Code immer mit entsprechenden Präprozessor-Direktiven geschützt werden. Es sollten Fallback-Lösungen für Umgebungen bereitgestellt werden, die dezimale Typen nicht unterstützen.
-
Keine impliziten Konvertierungen: Zwischen binären und dezimalen Typen sind keine impliziten Konvertierungen erlaubt. Bei expliziten Umwandlungen kann es zu Präzisionsverlusten kommen. Es ist daher ratsam, die Notwendigkeit jeder Konvertierung zu hinterfragen.
-
Präzisionsgrenzen beachten: Die Konstanten aus
<float.h>wieDEC32_MANT_DIG(für_Decimal32typischerweise 7 Dezimalstellen),DEC64_MANT_DIG(typischerweise 16 Dezimalstellen) undDEC128_MANT_DIG(typischerweise 34 Dezimalstellen) geben die verfügbare Genauigkeit an und helfen, Überläufe oder Rundungsprobleme korrekt einzuschätzen. -
Performance: Dezimale Gleitkommaoperationen sind oft langsamer als binäre, da sie auf gängigen Prozessoren wie x86, z. B. Intel oder AMD, keine native Hardware-Unterstützung haben und stattdessen softwareemuliert werden müssen, was zu höherem Rechenaufwand und reduzierter Geschwindigkeit führt. Dies ist besonders in performance-kritischen Anwendungen zu berücksichtigen. (Quelle: Native Hardware-Unterstützung existiert derzeit nur in IBM-Systemen, wie den POWER-Prozessoren und der z-Series. Es gibt auch Ansätze bei ARM (Helium Extension), aber im Mainstream praktisch nicht.)
-
Rundungsmodi: Dezimale Rundungsmodi sind über
<fenv.h>verfügbar, sofern dezimale Kommadatentypen unterstützt werden. Die konkreten Makronamen können implementierungsabhängig sein und sollten per Feature-Makros geprüft werden.
Dezimale Datentypen sind besonders geeignet für Finanzsoftware, wissenschaftliche Anwendungen oder rechtlich relevante Berechnungen, bei denen Rundungsfehler inakzeptabel sind. Da es sich jedoch um ein optionales Sprachfeature handelt, ist zusätzlicher Aufwand nötig, um sie plattformübergreifend zu unterstützen. Die strikte Typsicherheit und die separaten Bibliotheksfunktionen erfordern eine bewusste Auseinandersetzung mit dem Typ-System, bieten dafür aber die Gewissheit exakter dezimaler Arithmetik ohne die typischen Rundungsfehler binärer Gleitkommazahlen.
Fallback-Strategien
Wenn optionale Gleitkommatypen nicht verfügbar sind, empfiehlt es sich, klare Fallback-Strategien zu wählen. Für die binären Typen gilt: _Float32 entspricht fast immer float, _Float64 fast immer double. Fehlt _Float128, nutzt man in der Regel long double, falls dieses mehr Präzision bietet; andernfalls bleibt nur double oder eine externe Bibliothek. _Float16 kann notfalls auf float abgebildet werden, allerdings geht dabei die Speicherersparnis verloren und evtl. auch das garantierte Rundungsverhalten.
Bei dezimalen Typen (_Decimal32, _Decimal64, _Decimal128) sollte man nicht stillschweigend auf binäre Typen ausweichen, da sonst der zentrale Vorteil, die exakte Dezimalarithmetik, verloren geht. Wenn dezimale Genauigkeit fachlich erforderlich ist, etwa in Finanz- oder Rechtsanwendungen, bleibt nur die Wahl zwischen einer Fixed-Point-Darstellung (zum Beispiel mit int64_t und einem Skalierungsfaktor wie 10^2^) oder einer externen Decimal-Bibliothek. Ist Dezimalarithmetik nur optional, kann ein dokumentierter Wechsel auf binäre Typen vertretbar sein.
Best Practice ist, die Typauswahl über zentrale Aliasse zu kapseln und die Verfügbarkeit per Präprozessor-Makros zu prüfen. So bleibt der restliche Code portabel und übersichtlich, während an einer Stelle klar geregelt ist, welche Alternativen im Fallback genutzt werden.