Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perl for loop going haywire [duplicate]

Tags:

for-loop

perl

I have a simple for loop in Perl

for ($i=0; $i <= 360; $i += 0.01)
{
print "$i ";
}

Why is it that when I run this code I get the following output, where as soon as it gets to 0.81 it suddenly starts to add in a load more decimal places? I know I could simply round up to avoid this issue but I was wondering why it happens. An increment of 0.01 does not seem at all crazy to do.

 0.77
 0.78
 0.79
 0.8
 0.81
 0.820000000000001
 0.830000000000001
 0.840000000000001
 0.850000000000001
 0.860000000000001
 0.870000000000001
like image 420
DevilWAH Avatar asked Feb 06 '13 23:02

DevilWAH


Video Answer


4 Answers

Computers use binary representations. Not all decimal floating point numbers have exact representations in binary notation, so some error can occur (its actually a rounding difference). This is the same reason why you shouldn't use floating point numbers for monetary values:

messed up recepit

(Picture taken from dailywtf)

Most elegant way to get around this issue is using integers for calculations, dividing them to the correct number of decimal places and using sprintf to limit the number of decimal places printed. This will both make sure:

  • There's always to correct result printed
  • The rounding error doesn't accumulate

Try this code:

#!/usr/bin/perl
for ($i=0; $i <= 360*100; $i += 1) {
  printf "%.2f \n", $i/100;
}
like image 130
Jens Erat Avatar answered Oct 25 '22 05:10

Jens Erat


Basically, because the decimal number 0.01 does not have an exact representation in binary floating point, so over time, adding the best approximation to 0.01 deviates from the answer you'd like.

This is basic property of (binary) floating point arithmetic and not peculiar to Perl. What Every Computer Scientist Should Know About Floating-Point Arithmetic is the standard reference, and you can find it very easily with a Google search.

See also: C compiler bug (floating point arithmetic) and no doubt a myriad other questions.


Kernighan & Plauger say, in their old but classic book "The Elements of Programming Style", that:

  • A wise old programmer once said "floating point numbers are like little piles of sand; every time you move one, you lose a little sand and gain a little dirt".

They also say:

  • 10 * 0.1 is hardly ever 1.0

Both sayings point out that floating point arithmetic is not precise.

Note that some modern CPUs (IBM PowerPC) have IEEE 754:2008 decimal floating point arithmetic built-in. If Perl used the correct types (it probably doesn't), then your calculation would be exact.

like image 26
Jonathan Leffler Avatar answered Oct 25 '22 07:10

Jonathan Leffler


To demonstrate Jonathan's answer, if you code up the same loop structure in C, you will get the same results. Although C and Perl may compile differently and be ran on different machines the underlying floating point arithmetic rules should cause consistent outputs. Note: Perl uses double-precision floating point for its floating point representation whereas in C the coder explicitly chooses float or double.

Loop in C:

    #include <stdio.h>

    int main() {
        double i;
        for(i=0;i<=1;i+=.01)  {
          printf("%.15f\n",i);
        } 
      }

Output:

    0.790000000000000
    0.800000000000000
    0.810000000000000
    0.820000000000001
    0.830000000000001
    0.840000000000001
    0.850000000000001

To demonstrate the point even further, code the loop in C but now use single-precision floating point arithmetic and see that the output is less precise and even more erratic.

Output:

    0.000000000000000
    0.009999999776483 
    0.019999999552965
    0.029999999329448
    0.039999999105930
    0.050000000745058
    0.060000002384186
    0.070000000298023
    0.079999998211861
    0.089999996125698
    0.099999994039536
like image 25
Josh Avatar answered Oct 25 '22 06:10

Josh


1/10 is periodic in binary just like 1/3 is periodic in decimal. As such, it cannot be accurately stored in a floating point number.

>perl -E"say sprintf '%.17f', 0.1"
0.10000000000000001

Either work with integers

for (0*100..360*100) {
   my $i = $_/100;
   print "$i ";
}

Or do lots of rounding

for (my $i=0; $i <= 360; $i = sprintf('%.2f', $i + 0.01)) {
   print "$i ";
}
like image 30
ikegami Avatar answered Oct 25 '22 05:10

ikegami