Unexpected result using POSIX ceil() in Perl

与世无争的帅哥 提交于 2019-12-08 01:48:47

问题


I can't for the life of me figure out why the following produces the result it does.

use POSIX;
my $g = 6.65;
my $t = $g * 4;
my $r = $t - $g;
my $n = $r / $g;
my $c = ceil($n);
print "$c ($n)\n";

Sigil-tastic, I know — sorry.

I've solved this for my app as follows:

use POSIX;
my $g = 6.65;
my $t = $g * 4;
my $r = $t - $g;
my $n = $r / $g;
my $c = ceil("$n");
print "$c ($n)\n";

...but I'm bewildered as to why that's necessary here.


回答1:


Some numbers (like 6.65) have an exact representation in decimal, but cannot be represented exactly in the binary floating point that computers use (just like 1/3 has no exact decimal representation). As a result, floating point numbers are frequently slightly different than what you would expect. The result of your calculation is not 3, but about 3.000000000000000444.

The traditional way of handling this is to define some small number (called epsilon), and then consider two numbers equal if they differ by less than epsilon.

Your solution of ceil("$n") works because Perl rounds a floating point number to around 14 decimal places when converting it to a string (thus converting 3.000000000000000444 back to 3). But a faster solution would be to subtract epsilon (since ceil will round up) before computing ceil:

my $epsilon = 5e-15; # Or whatever small number you feel is appropriate
my $c = ceil($n - $epsilon);

A floating point subtraction should be faster than converting to a string and back (which involves a lot of division).




回答2:


What's happening is this: $n contains a floating point value, and thus is not exactly equal to 3, on my computer it's 3.00000000000000044409. Perl is smart enough to round that to 3 when printing it, but when you explicitly use a floating point function, it will do exactly what it advertises: ceil that to the next integral number: 4.

This is a reality of working with floating point numbers, and by no means Perl specific. You shouldn't rely on their exact value.




回答3:


use strict;
use warnings;
use POSIX;

my $g = 6.65;
my $t = $g * 4;
my $r = $t - $g;
my $n = $r / $g;  # Should be exactly 3.

# But it's not.
print "Equals 3\n" if $n == 3;

# Check it more closely.
printf "%.18f\n", $n;

# So ceil() is doing the right thing after all.
my $c = ceil($n);
print "g=$g t=$t r=$r n=$n c=$c\n";



回答4:


Obligatory Goldberg reference: What Every Computer Scientist Should Know About Floating-Point Arithmetic.

Using Perl, the ability to treat a string as a number in a numerical operation turns into an advantage because you can easily use sprintf to explicitly specify the amount of precision you want:

use strict; use warnings;

use POSIX qw( ceil );
my $g = 6.65;
my $t = $g * 4;
my $r = $t - $g;
my $n = $r / $g;
my $c = ceil( sprintf '%.6f', $n );
print "$c ($n)\n";

Output:

C:\Temp> g
3 (3)

The problem comes about because, given the finite number of bits available to represent a number, there are only a large but finite number of numbers that can be represented in floating point. Given that there are uncountably many numbers on the real line, this will result in approximation and rounding errors in the presence of intermediate operations.




回答5:


Not a Perl problem, as such

#include <stdlib.h>
#include <math.h>
#include <stdio.h>
main()
{
  double n = (6.65 * 4.0 - 6.65) / 6.65;
  double c = ceil(n);
  printf("c is %g, n was %.18f\n", c, n);
}

c is 4, n was 3.000000000000000444



回答6:


The other answers have explained why the problem is there, there's two ways to make it go away.

If you can, you can compile Perl to use higher precision types. Configuring with -Duse64bitint -Duselongdouble will make Perl use 64 bit integers and long doubles. These have high enough precision to make most floating point problems go away.

Another alternative is to use bignum which will turn on transparent arbitrary precision number support. This is slower, but precise, and can be used lexically.

{
    use bignum;
    use POSIX;
    my $g = 6.65;
    my $t = $g * 4;
    my $r = $t - $g;
    my $n = $r / $g;
    my $c = ceil($n);
    print "$c ($n)\n";
}

You can also declare individual arbitrary precision numbers using Math::BigFloat



来源:https://stackoverflow.com/questions/2379934/unexpected-result-using-posix-ceil-in-perl

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!