How to eliminate Perl rounding errors
Consider the following program:
$x=12345678901.234567000;
$y=($x-int($x))*1000000000;
printf("%f:%f\n",$x,$y);
Here's what is prints:
12345678901.234568:234567642.211914
I was expecting:
12345678901.234567:234567000
This appears to be some sort of rounding issue in Perl.
How could I 开发者_Python百科change it to get234567000
instead?
Did I do something wrong?This is a frequently-asked question.
Why am I getting long decimals (eg, 19.9499999999999) instead of the numbers I should be getting (eg, 19.95)?
Internally, your computer represents floating-point numbers in binary. Digital (as in powers of two) computers cannot store all numbers exactly. Some real numbers lose precision in the process. This is a problem with how computers store numbers and affects all computer languages, not just Perl.
perlnumber shows the gory details of number representations and conversions. To limit the number of decimal places in your numbers, you can use the
printf
orsprintf
function. See the Floating Point Arithmetic for more details.printf "%.2f", 10/3; my $number = sprintf "%.2f", 10/3;
Make "use bignum;" the first line of your program.
Other answers explain what to expect when using floating point arithmetic -- that some digits towards the end are not really part of the answer. This is to make the computations do-able in a reasonable amount of time and space. If you are willing to use unbounded time and space to work with numbers, then you can use arbitrary-precision numbers and math, which is what "use bignum" enables. It's slower and uses more memory, but it works like math you learned in elementary school.
In general, it's best to learn more about how floating point math works before converting your program to arbitrary-precision math. It's only needed in very strange situations.
The whole issue of floating point precision has been answered, but you're still seeing the problem despite bignum
. Why? The culprit is printf
. bignum
is a shallow pragma. It only affects how numbers are represented in variables and math operations. Even though bignum
makes Perl do the math right, printf
is still implemented in C. %f
takes your precise number and turns it right back into an imprecise floating point number.
Print your numbers with just print
and they should do fine. You'll have to format them manually.
The other thing you can do is to recompile Perl with -Duse64bitint -Duselongdouble
which will force Perl to internally use 64 bit integers and long double
floating point numbers. This will give you a lot more accuracy, more consistently and almost no performance cost (bignum
is a bit of a performance hog for math intensive code). Its not 100% accurate like bignum
, but it will affect things like printf
. However, recompiling Perl this way makes it binary incompatible, so you're going to have to recompile all your extensions. If you do this, I suggest installing a fresh Perl in a different location (/usr/local/perl/64bit
or something) rather than trying to manage parallel Perl installs sharing the same library.
Homework (Googlework?) for you: How are floating point numbers represented by computers?
You can only have a limited number of precise digits, everything beyond that is just the noise from base conversion (binary to decimal). That is also why the last digit of your $x
appears to be 8
.
$x - (int($x)
is 0.23456linenoise
, which is also a floating point number. Multiplied by 1000000000, it gives another floating point number, with more random digits pulled from the incommensurability of the bases.
Perl does not do arbitrary precision arithmetic for its built-in floating point types. So your initial variable $x
is an approximation. You can see this by doing:
$ perl -e 'printf "%.10f", 12345678901.234567000'
12345678901.2345676422
This answer works on my x64 platform, by accommodating the scale of the errors
sub safe_eq {
my($var1,$var2)=@_;
return 1 if($var1==$var2);
my $dust;
if($var2==0) { $dust=abs($var1); }
else { $dust= abs(($var1/$var2)-1); }
return 0 if($dust>5.32907051820076e-15 ); # 5.32907051820075e-15
return 1;
}
You can build on the above to solve most of your problems.
Avoid bignum if you can - it's stupendously slow - plus it will not solve any problems if you've got to store your numbers anyplace like a DB or in JSON etc.
This has to do with the (limited) accuracy of the floating point computations a computer does. Generally when comparing floating point numbers you should compare with a suitable epsilon:
$value1 == $value2 or warn;
won't work as expected in most cases. You should do
use constant EPSILON => 1.0e-10;
abs($value1 - $value2) < EPSILON or warn;
EPSILON should be chosen such that it takes into account the complexity of the computations for valueX. A large computation might lead to a much, much larger EPSILON.
The other option is, as suggested by others:
sprintf("%.5f", value1) eq sprintf("%.5f", value2) or warn;
Or use an arbitrary precision math library.
精彩评论