Как устранить неточность с плавающей запятой при упаковке и распаковке числа с плавающей запятой?

Я упаковываю массив чисел для отправки через UDP на другое оборудование, используя программирование сокетов.

Когда я pack число 12,2, а затем unpack, я получаю 12,199999892651. Поскольку я работаю с числами, связанными с широтами и долготами, у меня не может быть таких отклонений.

Это простой сценарий, который я написал:

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";

Результат:

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

Есть ли способ контролировать точность?


person Asheesh    schedule 01.09.2016    source источник
comment
Я думаю, вам следует выделить свой второй вопрос в отдельный пост; это не совсем связано с первым вопросом. Так будущим посетителям будет легче найти ответы.   -  person ThisSuitIsBlackNot    schedule 01.09.2016
comment
12,2 — десятичное число, равное 122/10. Но он не может быть представлен выражением m/(2**n) и, следовательно, не может быть точно представлен с использованием типов данных, основанных на типах с плавающей запятой C. Точность теряется в тот момент, когда ваш код сталкивается с числом 12,2 — внутри не существует такого понятия, как значение с плавающей запятой 12,2. У нас есть похожие проблемы с основанием 10, но мы к ним привыкли, поэтому они нас не удивляют. Примером может быть выражение 1/3 в десятичной форме: 0,33? По некоторым параметрам близко, но не точно.   -  person DavidO    schedule 01.09.2016
comment
Ваш массив @Data искусственный. Обычно он будет состоять из текстовых строк, а не чисел с плавающей запятой. Я думаю, вам нужно отправить текст "@Data" вместо того, чтобы пытаться упаковать значения как с плавающей запятой. Другой конец, каким бы он ни был, может легко разделить строку на пробелы и восстановить именно то, что было отправлено.   -  person Borodin    schedule 01.09.2016
comment
Пожалуйста, всегда use strict, даже в самых простых программах Perl   -  person Borodin    schedule 01.09.2016
comment
Спасибо.. я задам его как второй вопрос.. @ThisSuitIsBlackNot   -  person Asheesh    schedule 01.09.2016
comment
Если предположить, что вы имеете дело с широтой и долготой в градусах на Земле, то ошибка между 12,2 и 12,199999892651 на экваторе составляет 12 мм.   -  person    schedule 01.09.2016


Ответы (3)


Шаблон f pack предназначен для чисел с плавающей запятой одинарной точности, которые на большинстве платформ подходят с точностью до 7 знаков после запятой или около того. Шаблон d предлагает двойную точность и будет достаточно для ~15 знаков после запятой.

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"
person mob    schedule 01.09.2016

Короткий ответ: не упаковывайте эти числа как числа с плавающей запятой. Вы потеряете точность из-за представления с плавающей запятой IEEE. Вместо этого преобразуйте их в «десятичные знаки» (т. е. строки) и упакуйте их как строки. Если вам действительно нужна точность и вам не нужно выполнять над ними математические операции, вы можете сохранить их как строки в Perl.

person mwp    schedule 01.09.2016
comment
Принимающий аппаратный конец может принимать только значения... Он не может принимать строки - person Asheesh; 01.09.2016
comment
Тогда, я думаю, вам придется смириться с некоторой потерей точности. Вы можете попробовать формат пакета d (двойной) вместо f. - person mwp; 01.09.2016
comment
@Asheesh: если принимающая сторона может принимать только упакованные числа с плавающей запятой, то вы застряли с потерей точности. - person Borodin; 01.09.2016
comment
@Бородин Джинкс. :) - person mwp; 01.09.2016

2/10 — это периодическое число в двоичном формате, точно так же, как 1/3 — это периодическое число в десятичном виде. Невозможно сохранить его точно в числе с плавающей запятой, так как это займет бесконечное хранилище.

Таким образом, не pack вызывает ошибку; он точно хранит именно тот номер, который вы ему предоставили.

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

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

Пока вы используете числа с плавающей запятой, вы не сможете точно хранить 12.2.

Но, как вы можете видеть выше, вы можете сохранить хранилище достаточно точно, используя d (двойная точность, точность почти 16 цифр) вместо f (одинарная точность, точность более 7 цифр). Perl использует двойную точность, так что вы фактически вводили потерю точности, используя f вместо d.

Поэтому используйте d и округляйте результаты (sprintf "%.10f").

person ikegami    schedule 02.09.2016