简体   繁体   中英

How can I eliminate floating point inaccuracy when I pack and unpack a floating point number?

I am packing an array of numbers to send via UDP to another piece of hardware using socket programming.

When I pack the number 12.2 and then unpack it, I get 12.199999892651. As I am working with numbers related to latitudes and longitudes, I cannot have such deviations.

This is the simple script I wrote:

use warnings;

use Time::HiRes qw (sleep);

@Data = ( 20.2, 30.23, 40.121, 1, 2, 3, 4, 6. 4, 3.2, 9.9, 0.1, 12.2, 0.99, 7.8, 999, 12.3 );

$myArr = pack('f*', @Data);

print "$myArr\n\n";

@Dec = unpack('f*',$myArr);

print "@Dec";

The output is:

20.2000007629395 30.2299995422363 40.1209983825684 1 2 3 4 6.40000009536743 3.20 000004768372 9.89999961853027 0.100000001490116 12.1999998092651 0.9900000095367 43 7.80000019073486 999 12.3000001907349

Is there any way I can control the precision?

pack 's f template is for single-precision floating point numbers, which on most platforms is good to 7 or so decimal places of accuracy. The d template offers double-precision and will be good enough for ~15 decimal places.

print unpack("f", pack("f",12.2));          # "12.1999998092651"
print unpack("d", pack("d",12.2));          # "12.2"

printf "%.20f",unpack("f", pack("f",12.2)); # "12.19999980926513671875"
printf "%.20f",unpack("d", pack("d",12.2)); # "12.19999999999999928946"

The short answer is: don't pack these numbers as floats. You will lose accuracy due to IEEE floating point representation. Instead, convert them to "character decimals" (ie strings), and pack them as strings. If you really need the accuracy, and don't need to perform math operations on them, you may want to store them as strings in Perl as well.

2/10 is a periodic number in binary just like 1/3 is a periodic number in decimal. It's impossible to store it exactly in a floating point number as it would take infinite storage.

As such, it's not pack that's introducing the error; it's faithfully storing precisely the number you provided it.

$ perl -E'say sprintf "%.20e", 12.2'
1.21999999999999992895e+01

$ perl -E'say sprintf "%.20e", unpack "d", pack "d", 12.2'
1.21999999999999992895e+01

As long as you use floating point numbers, you will not be able to store 12.2 exactly.

But as you can see above, you can store store precisely enough by using d (double-precision, almost 16 digits of precision) instead of f (single-precision, over 7 digits of precision). Perl uses double-precision, so you were actually introducing precision loss by using f instead of d .

So use d , and round your results ( sprintf "%.10f" ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM