Gebruik double in plaats van float.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de Haagse Hogeschool - TH Rijswijk/Academie voor Engineering.

In het boek "De taal C - van PSD tot C-programma" van Daryl McAllister wordt voor het opslaan van een reëel getal een variabele van het type float gebruikt. Lang geleden (toen het boek werd geschreven) was dit inderdaad gebruikelijk. Ik adviseer je echter om in plaats van het type float het type double te gebruiken.

Nauwkeurigheid van floating point getallen.

Variabelen van het type float en variabelen van het type double zijn allebei bedoeld om reële getallen in op te slaan. Het verschil zit in het aantal bits dat voor de opslag wordt gebruikt. Een variabele van het type float wordt opgeslagen in 32 bits (4 bytes). Een variabele van het type double wordt opgeslagen in 64 bits (8 bytes). Een double heeft dus dubbel zoveel bits als een float. Dit verklaart meteen de naam van dit type. Doordat een double uit meer bits bestaat kunnen er grotere getallen in worden opgeslagen. Nog veel belangrijker is echter dat een double meer significante cijfers heeft en dus veel nauwkeuriger is. Een float heeft 7 en een double heeft 15 significante cijfers. In het onderstaande programma wordt de waarde 100.0 / 3.0 berekent als float en als double en afgedrukt met lekker veel cijfers achter de decimale punt.

#include <stdio.h>

int main() {
   float f=100.0/3.0;
   double d=100.0/3.0;
   printf("f = %.20f\n", f);
   printf("d = %.20lf\n", d);
   getchar();
   return 0;
}

Dit programma geeft als het gecompileerd wordt met gcc (de compiler die wordt gebruikt door wxDev-C++) de volgende uitvoer:

f = 33.33333206176757800000
d = 33.33333333333333600000

Zoals je weet kunnen float variabelen worden afgedrukt met printf door de code %f te gebruiken. Voor variabelen van het type double moet %lf gebruikt worden. Je ziet dat een double veel nauwkeuriger is dan een float.

Getallen van het type float en double worden in de computer in het binaire talstelsel opgeslagen. Alle breuken die niet als a / 2b geschreven kunnen worden (waarbij a en b gehele getallen zijn) moeten dan afgerond worden. In het tientallig stelsel moeten alle breuken die niet als a / 10c geschreven kunnen worden (waarbij a en c gehele getallen zijn) afgerond worden. Omdat 10c gelijk is aan 2c x 5c moeten alle getallen die in het decimale stelsel afgerond moeten worden in het binaire stelsel ook afgerond worden. Maar sommige getallen die in het decimale stelsel niet moeten worden afgerond moeten in het binaire stelsel wel afgerond worden. Bijvoorbeeld 101/10 = 10.1.

#include <stdio.h>

int main() {
   float f=101.0/10.0;
   double d=101.0/10.0;
   printf("f = %.20f\n", f);
   printf("d = %.20lf\n", d);
   getchar();
   return 0;
}

Dit programma geeft als het gecompileerd wordt met gcc de volgende uitvoer:

f = 10.10000038146972656200
d = 10.09999999999999964500

Je ziet dat het getal 10.1 ook moet worden afgerond.

Vergelijken van floating point getallen.

In het boek wordt gewaarschuwd dat het vergelijken van floating point getallen met == en != gevaarlijk is omdat het resultaat door afrondfouten anders kan zijn dan je zou verwachten. Het onderstaande programma geeft bijvoorbeeld de uitvoer:

Dat verwacht je niet!

Het maakt daarbij overigens niet uit of je float of double gebruikt.

#include <stdio.h>

int main() {
   float f=1.0/3.0;
   if (f + f + f != 1.0)
   	  printf("Dat verwacht je niet!\n");
   getchar();
   return 0;
}

In het boek wordt alleen gesproken over problemen bij het vergelijken met == en !=. Ook bij alle andere vergelijkingsoperatoren kunnen vergelijkbare problemen optreden.

Als voorbeeld is hieronder een programma dat een conversie tabel aanmaakt voor het omzetten van temperaturen van °F naar °C gegeven:

#include <stdio.h>

int main() {
   float fahrenheit;
   float celcius;
   printf("     F      C\n");
   for (fahrenheit=31.0; fahrenheit<=32.0; fahrenheit=fahrenheit+0.1) {
      celcius=(fahrenheit-32.0)/1.8;
      printf("%6.2f %6.2f\n", fahrenheit, celcius);
   }
   getchar();
   return 0;
}

De uitvoer van dit programma is als volgt:

     F      C
 31.00  -0.56
 31.10  -0.50
 31.20  -0.44
 31.30  -0.39
 31.40  -0.33
 31.50  -0.28
 31.60  -0.22
 31.70  -0.17
 31.80  -0.11
 31.90  -0.06

Er wordt dus geen  regel afgedrukt voor fahrenheit is 32 en dat zou je wel verwachten omdat de voorwaarde fahrenheit<=32.0 gebruikt wordt! Het is overigens toeval dat in dit geval de laatste regel uit de tabel niet wordt afgedrukt. Als we de start en stopwaarde allebei met 1.0 verhogen wordt de laatste regel wel afgedrukt.

#include <stdio.h>

int main() {
   float fahrenheit;
   float celcius;
   printf("     F      C\n");
   for (fahrenheit=32.0; fahrenheit<=33.0; fahrenheit=fahrenheit+0.1) {
      celcius=(fahrenheit-32.0)/1.8;
      printf("%6.2f %6.2f\n", fahrenheit, celcius);
   }
   getchar();
   return 0;
}

De uitvoer van dit programma is als volgt:

     F      C
 32.00   0.00
 32.10   0.06
 32.20   0.11
 32.30   0.17
 32.40   0.22
 32.50   0.28
 32.60   0.33
 32.70   0.39
 32.80   0.44
 32.90   0.50
 33.00   0.56

Ook het gebruik van < in plaats van <= helpt ons niets. De uitvoer van beide bovenstaande programma's is namelijk exact gelijk als we de <= vervangen door een <. De uitvoer van het eerste programma is dan correct (de regel voor fahrenheit is 32 wordt dan zoals je verwacht niet afgedrukt) maar de uitvoer van het tweede programma is dan onverwachts (de regel voor fahrenheit is 33 wordt dan toch wel afgedrukt).

Conclusie: Vergelijken met een grenswaarde is onvoorspelbaar bij het gebruik van floating point getallen!

De enige methode die een voorspelbaar resultaat oplevert is om te vergelijken met een waarde tussen twee stappen in en niet met een grenswaarde. Dus gebruik in het eerste programma de for lus:

   for (fahrenheit=31.0; fahrenheit<32.05; fahrenheit=fahrenheit+0.1) {

en in het tweede programma de for lus.

   for (fahrenheit=32.0; fahrenheit<33.05; fahrenheit=fahrenheit+0.1) {

Beide programma's zullen nu altijd de tabel helemaal zoals je verwacht printen.

Geschiedenis.

De geschiedenis van de microprocessor kun je bekijken op: http://www.computerhistory.org/exhibits/microprocessors/. De eerste 32 bits Intel processor was de 80386 (1985). Deze processor kon (in de hardware) alleen met integer getallen werken. Als je met floating point getallen wilde rekenen dan moesten deze floating point berekeningen (in de software) worden opgesplitst in integer berekeningen die in de hardware werden uitgevoerd. Het rekenen met floating point getallen was met de 80386 dus erg traag ook al omdat deze microprocessor werkte op een klokfrequentie van 16 MHz. Om sneller te kunnen rekenen met floating point getallen was een speciale chip (de 80387) verkrijgbaar die naast de 80386 op het moederbord kon worden geplaatst en die wel in staat was om de floating point berekeningen in hardware uit te voeren. Omdat de meeste PC's in die tijd niet beschikten over z'n extra rekenchip was het gebruik van float in plaats van double in die tijd aan te raden omdat de 80386 float bewerkingen veel sneller kon uitvoeren (in software) dan double bewerkingen. Gebruikers die wel een 80387 in hun systeem hadden konden echter, ook toen al, beter gebruik maken van double dan van float omdat de 80387 (bijna) net zo snel met double's kon werken als met float's.

In 1989 verscheen de 80486 (25 MHz) van Intel die een ingebouwde floating point unit (FPU) had waarmee floating point bewerkingen in hardware konden worden uitgevoerd. Vanaf die tijd kun je dus voor floating point berekeningen beter variabelen van het type double gebruiken omdat deze berekeningen (bijna) net zo snel zijn, maar wel 2 maal zo nauwkeurig zijn, als berekeningen met variabelen van het type float. Voor de opslag van grote hoeveelheden floating point getallen waarbij niet zoveel significante cijfers nodig zijn wordt het type float nog wel gebruikt omdat 1 double net zoveel ruimte inneemt als 2 float's. Later (in 1991) heeft Intel overigens nog een low cost variant van de 80486 op de markt gebracht (de zogenaamde 80486SX, de originele 80486 werd toen omgedoopt tot de 80486 DX). Deze 80486SX bleek een 80486DX met een kapotte FPU te zijn ;-)

Citaat van: http://www.x86.org/articles/computalk/help.htm

The 80486 offered little in the way of architectural enhancements over its 80386 predecessor. The most significant enhancement of the 486 family was the integration of the 80387-math coprocessor into the 80486-core logic. Now, all software that requires the math coprocessor could run on the 80486 without any expensive hardware upgrades. Like the 80386 SX, Intel decided to introduce the 80486 SX as a cost-reduced 80486 DX. Unfortunately, Intel chose to ensure that these processors were neither pin-compatible, nor 100% software compatible with each other. Unlike the 80386 SX, the 80486 SX enjoyed the full data bus and address bus of its DX counterpart. Instead, Intel removed the math coprocessor, thereby rendering the 80486 SX somewhat software incompatible with its DX counterpart. To further complicate matters, Intel introduced the 80487 SX -- the "math coprocessor" to the 80486 SX. Intel convinced vendors to include a new socket on the motherboard that could accommodate the 80486 SX and 80487 SX as an expensive hardware upgrade option. Unbeknownst to the consumer, the 80486 SX was an 80486 DX with a non-functional math unit (though later versions of the chip actually removed the math unit). The 80487 SX was a full 80486 DX with a couple of pins relocated on the package -- to prevent consumers from using the cheaper 80486 DX as an upgrade option. In this regard, Intel created a marketing deception. Intel marketed the 80487 SX as a math coprocessor to the 80486 SX. In reality, the 80487 SX electronically disabled the 80486 SX when installed, thereby relegating this chip to the status of an expensive space heater. Sadly, the consumer never knew or even suspected Intel of playing such manipulative games.

Vanaf de Intel Pentium (de opvolger van de 80486) heeft elke Intel processor een (of meer) floating point unit(s) aan boord. De FPU van de Pentium werd zelf beroemd vanwege de zogenaamde Pentium bug. Zie: http://www.byte.com/art/9503/sec13/art1.htm