Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why 0.1 + 0.2 != 0.3, or the mysterious world o...

Why 0.1 + 0.2 != 0.3, or the mysterious world of floating-point numbers.

Yes, running the above expression in PHP, and in a lot of other languages, will confirm that 0.1 + 0.2 does not equal to 0.3.

But this weird behavior is explainable and even justified.

Floating point numbers can open you up a completely new universe.
Provided of course that they are properly used.

This talk will go through a bit of theory, and a lot of concrete examples.

From the Gulf War to the Quake graphics engine, via the Ariane 5 space rocket, we will see the benefits and the potential dangers of using floating-point numbers.

From these examples, we will extract some lessons we can translate into our day to day development practices.

And finally, the talk will cover when to use floating-point numbers, and when to avoid them, like when managing prices or billing, and what are the alternatives in those cases.

Benoit Jacquemont

October 23, 2020
Tweet

More Decks by Benoit Jacquemont

Other Decks in Programming

Transcript

  1. Why Why 0.1 + 0.2 != 0.3 0.1 + 0.2

    != 0.3 The Mysterious World of Floating Point Numbers The Mysterious World of Floating Point Numbers Benoit Jacquemont Benoit Jacquemont @bjacquemont @bjacquemont
  2. <?php if (0.1 + 0.2 != 0.3) { echo "0.1

    + 0.2 is NOT EQUAL to 0.3\n"; } 0.1 + 0.2 is NOT EQUAL to 0.3
  3. $sum = 0.1 + 0.2; echo "Sum is ".$sum."\n"; if

    ($sum != 0.3) { echo "$sum is NOT EQUAL to 0.3\n"; } Sum is 0.3 0.3 is NOT EQUAL to 0.3 GO HOME GO HOME PHP PHP, ,
  4. Let's try the same in JavaScript if (0.1 + 0.2

    != 0.3) { console.log('0.1 + 0.2 is NOT EQUAL to 0.3'); } 0.1 + 0.2 is NOT EQUAL to 0.3
  5. Let's continue with JavaScript... let sum = 0.1 + 0.2;

    console.log("Sum is " + sum); if (sum != 0.3) { console.log(sum + ' is NOT EQUAL to 0.3'); } Sum is 0.30000000000000004 0.30000000000000004 is NOT EQUAL to 0.3
  6. Why in PHP ? $sum = 0.1 + 0.2; echo

    "Sum is ".$sum."\n"; if ($sum != 0.3) { echo "$sum is NOT EQUAL to 0.3\n"; } Sum is 0.3 0.3 is NOT EQUAL to 0.3
  7. The precision con g option No impact on the precision

    of the computation! Round oating point numbers for display php.net/manual/en/ini.core.php#ini.precision
  8. Increasing how many digits are displayed ini_set('precision', 17); // default

    14 $sum = 0.1 + 0.2; echo "Sum is ".$sum."\n"; if ($sum != 0.3) { echo "$sum is NOT EQUAL to 0.3\n"; } Sum is 0.30000000000000004 0.30000000000000004 is NOT EQUAL to 0.3
  9. Our computers are Our computers are binary binary 0 0

    or or 1 1 How to represent something in How to represent something in between? between?
  10. Representing Real Numbers in Scienti c Notation 0.006458 6.458 x

    10-3 6 . 458 x 10 -3 6: most signi cant digit, always non zero 458: other signi cant digits -3: exponent 3 parts, each representable by integers
  11. Binary Implementation 0.0110111 1 . 10111 x 2 -10 Let's

    play with the exponent! Look ⇑, a oating point!
  12. Binary Floating Point to Decimal Representation 000000000110000000000000000000000000000000000000000000000010101 1.10101 x 23

    110.101 1 1 0 . 1 0 1 22= 4 21= 2 20= 1 2-1= ½ 2-2= ¼ 2-3= ⅛ 1.10101 x 22 = 4 + 2 + ½ + ⅛ = 6.625
  13. Largest integer: 9.22 x 10 Largest integer: 9.22 x 1018

    18 Earthworms mass: 7.6 x 10 Earthworms mass: 7.6 x 1012 12 kg kg
  14. Largest integer: 9.22 x 10 Largest integer: 9.22 x 1018

    18 Oceans mass: 1.42 x 10 Oceans mass: 1.42 x 1021 21 kg kg
  15. Largest integer: 9.22 x 10 Largest integer: 9.22 x 1018

    18 Earth mass: 5.42 x 10 Earth mass: 5.42 x 1024 24 kg kg
  16. Largest integer: 9.22 x 10 Largest integer: 9.22 x 1018

    18 Milky way mass: 2.98 x 10 Milky way mass: 2.98 x 1042 42 kg kg
  17. Largest integer: 9.22 x 10 Largest integer: 9.22 x 1018

    18 Observable Universe mass: Observable Universe mass: 1.5 x 10 1.5 x 1053 53 kg kg
  18. Largest oating point number echo number_format(PHP_FLOAT_MAX, 0, "", "")."\n"; 1797693134862315708145274237317043567980705675258449965989174768031

    5726078002853876058955863276687817154045895351438246423432132688946 4182768467546703537516986049910576551282076245490090389328944075868 5084551339423045832369032229481658085593321233482747978262041447231 68738177180919299881250404026184124858368 1.80 x 10308 290 orders of magnitude bigger than max integer
  19. Float range is immensely wider than integers Integer: 1 to

    9.2 x 1018 vs Float: 2.25 x 10-308 to 1.80 x 10308 But both use 64 bit storage...
  20. Why 0.1 + 0.2 == 0.30000000000000004 ini_set('precision', 20); $op1 =

    0.1; $op2 = 0.2; $expectedSum = 0.3; echo "Op #1: ".$op1."\n"; echo "Op #2: ".$op2."\n"; $actualSum = $op1 + $op2; echo "Actual Sum: ".$actualSum."\n"; echo "Expected Sum: ".$expectedSum."\n"; Op #1: 0.10000000000000000555 ← closest approximation of 0.1 Op #2: 0.2000000000000000111 ← closest approximation of 0.2 Actual Sum: 0.30000000000000004441 ← this is an approximation as well
  21. How to have a consistent behavior between applications and hardware?

    754 Standard So most computers and programming languages have the same behavior
  22. Ariane 5 Maiden Flight 4 June 1996 Payload: 4 satellites

    of 1.2 T each. Total cost: $500 million
  23. Same code as Ariane 4, but with 4x thrust... //64

    bits float float horizontalVelocity = Inertial.getHorizontalVelocity(); // conversion to 16 bits integer (max 32767) int correction = computeCorrection((int) horizontalVelocity); // overflow! applyHorizontalCorrection(correction);
  24. Same in PHP $myFloat = 600000000000000000000; $myInt = (int) $myFloat;

    echo $myInt."\n"; -8742554432415203328 No error, and the result doesn't make sense...
  25. The Gulf War & the The Gulf War & the

    Patriot Missile Event Patriot Missile Event
  26. // return the time in seconds in a float $timeBefore

    = getElapsedTimeFromStartup(); 1 2 3 sendRadarPingAndWaitForEcho(); 4 5 $timeAfter = getElapsedTimeFromStartup(); 6 7 $targetDistance = ($timeAfter - $timeBefore) * LIGHT_SPEED/2; 8 // return the time in seconds in a float $timeBefore = getElapsedTimeFromStartup(); sendRadarPingAndWaitForEcho(); 1 2 3 4 5 $timeAfter = getElapsedTimeFromStartup(); 6 7 $targetDistance = ($timeAfter - $timeBefore) * LIGHT_SPEED/2; 8 // return the time in seconds in a float $timeBefore = getElapsedTimeFromStartup(); sendRadarPingAndWaitForEcho(); $timeAfter = getElapsedTimeFromStartup(); 1 2 3 4 5 6 7 $targetDistance = ($timeAfter - $timeBefore) * LIGHT_SPEED/2; 8 // return the time in seconds in a float $timeBefore = getElapsedTimeFromStartup(); sendRadarPingAndWaitForEcho(); $timeAfter = getElapsedTimeFromStartup(); $targetDistance = ($timeAfter - $timeBefore) * LIGHT_SPEED/2; 1 2 3 4 5 6 7 8
  27. Float rule #2 The bigger a oat, the less precise

    it becomes aka Loss of precision
  28. // return the time in seconds in a float $timeBefore

    = getElapsedTimeFromStartup(); sendRadarPingAndWaitForEcho(); $timeAfter = getElapsedTimeFromStartup(); $targetDistance = ($timeAfter - $timeBefore) * LIGHT_SPEED/2; The longer the missile battery runs, the less precise getElapsedTimeFromStartup() becomes
  29. Missile battery run time: 100 hours Inaccuray due to loss

    of precision: 0.3433s Scud missile speed: Mach 5 Patriot missile offset to its target: 589m
  30. Quake Quake Released in Feb. 1997 Released in Feb. 1997

    Full real-time 3D Full real-time 3D rendering without 3D rendering without 3D accelerator accelerator Runs on a 60Mhz Runs on a 60Mhz Pentium Pentium
  31. About me E-COMMERCE MOBILE APPLICATION PRINT CATALOG POINTS OF SALE

    ERP MEDIA SERVER SUPPLIERS PURCHASING DPT MARKETING DPT CSV FTP XML XLS SUPPLIERS PORTAL ENRIC H TRAN SLATE CONTROL Single Source of Truth for Product Information
  32. Storing real numbers in database mysql> CREATE TABLE my_float(f FLOAT);

    mysql> INSERT INTO my_float(f) VALUES(0.1); mysql> SELECT * FROM my_float; +------+ | f | +------+ | 0.1 | +------+ mysql> SELECT * FROM my_float WHERE f = 0.1; Empty set (0.00 sec) mysql> SELECT ROUND(f, 17) FROM my_float; +---------------------+ | ROUND(f, 17) | +---------------------+ | 0.10000000149011612 | +---------------------+ Loss of data due to approximation
  33. Storing real numbers with Decimal mysql> CREATE TABLE my_decimal(d DECIMAL(10,5));

    mysql> INSERT INTO my_decimal(d) VALUES(0.1); mysql> SELECT * FROM my_decimal; +---------+ | d | +---------+ | 0.10000 | +---------+ mysql> SELECT * FROM my_decimal WHERE d = 0.1; +---------+ | d | +---------+ | 0.10000 | +---------+ No approximation with xed point type
  34. Transmitting real numbers ini_set('precision', 17); $myJsonFromAPI = '{"name": "foo", "price":

    29.99}'; $myObject = json_decode($myJsonFromAPI, false); echo "Price:".$myObject->price."\n"; Price:29.989999999999998 Loss of data due to approximation Use strings to avoid approximation
  35. Money, payment & billing $price = 49.99; $coupon = 20.25;

    $card = 29.74; 1 2 3 4 echo "Price: $price. Using coupon $coupon.\n"; 5 $remaining = $price - $coupon; 6 7 echo "$remaining still to pay. Using card $card.\n"; 8 $remaining = $remaining - $card; 9 10 if ($remaining == 0) { 11 echo "Thank you for your payment!\n"; 12 } else { 13 echo "Paiement not finished: $remaining to pay\n"; 14 } 15 echo "Price: $price. Using coupon $coupon.\n"; $remaining = $price - $coupon; $price = 49.99; 1 $coupon = 20.25; 2 $card = 29.74; 3 4 5 6 7 echo "$remaining still to pay. Using card $card.\n"; 8 $remaining = $remaining - $card; 9 10 if ($remaining == 0) { 11 echo "Thank you for your payment!\n"; 12 } else { 13 echo "Paiement not finished: $remaining to pay\n"; 14 } 15 echo "$remaining still to pay. Using card $card.\n"; $remaining = $remaining - $card; $price = 49.99; 1 $coupon = 20.25; 2 $card = 29.74; 3 4 echo "Price: $price. Using coupon $coupon.\n"; 5 $remaining = $price - $coupon; 6 7 8 9 10 if ($remaining == 0) { 11 echo "Thank you for your payment!\n"; 12 } else { 13 echo "Paiement not finished: $remaining to pay\n"; 14 } 15 $price = 49.99; $coupon = 20.25; $card = 29.74; echo "Price: $price. Using coupon $coupon.\n"; $remaining = $price - $coupon; echo "$remaining still to pay. Using card $card.\n"; $remaining = $remaining - $card; if ($remaining == 0) { echo "Thank you for your payment!\n"; } else { echo "Paiement not finished: $remaining to pay\n"; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Price: 49.99. Using coupon 20.25. 1 29.74 still to pay. Using card 29.74. 2 Paiement not finished: 3.5527136788005E-15 to pay 3 Price: 49.99. Using coupon 20.25. 29.74 still to pay. Using card 29.74. 1 2 Paiement not finished: 3.5527136788005E-15 to pay 3 Price: 49.99. Using coupon 20.25. 29.74 still to pay. Using card 29.74. Paiement not finished: 3.5527136788005E-15 to pay 1 2 3
  36. Money, payment & billing ini_set('precision', 17); //Only change! $price =

    49.99; $coupon = 20.25; $card = 29.74; echo "Price: $price. Using coupon $coupon.\n"; $remaining = $price - $coupon; echo "$remaining still to pay. Using card $card.\n"; $remaining = $remaining - $card; if ($remaining == 0) { echo "Thank you for your payment!\n"; } else { echo "Paiement not finished: $remaining to pay\n"; } Price: 49.990000000000002. Using coupon 20.25. 29.740000000000002 still to pay. Using card 29.739999999999998. Paiement not finished: 3.5527136788005009E-15 to pay
  37. Use cents and integers $price = 4999; $coupon = 2025;

    $card = 2974; function f(int $amount): string { return substr($amount, 0, -2).'.'.substr($amount, -2); } echo "Price:".f($price).". Using coupon ".f($coupon).".\n"; $remaining = $price - $coupon; echo f($remaining).' still to pay. Using card '.f($card)."\n"; $remaining = $remaining - $card; if ($remaining == 0) { echo "Thank you for your payment!\n"; } else { echo "Payement not finished.\nRemaining amount to pay:".f($remaining)."\n"; } Price:49.99. Using coupon 20.25. 29.74 still to pay. Using card 29.74 Thank you for your payment!
  38. BCMath Arbitrary Precision Mathematics bcscale(2); $sum = bcadd("0.1", "0.2"); //

    strings!! if (bccomp($sum, "0.3")) { echo "$sum is NOT EQUAL to 0.3\n"; } else { echo "$sum is EQUAL to 0.3\n"; } 0.30 IS EQUAL to 0.3 Computation Comparison Needs an extension
  39. php decimal Arbitrary-Precision Decimal Arithmetic For PHP 7 use Decimal\Decimal;

    $op1 = new Decimal("0.1"); $op2 = new Decimal("0.2"); $sum = $op1->add($op2); if (!$sum->equals("0.3")) { echo "$sum is NOT EQUAL to 0.3\n"; } else { echo "$sum is EQUAL to 0.3\n"; } 0.3 is EQUAL to 0.3 Nice object interface Not easily available php-decimal.io