Почему умножение и деление на N фиксируют представление с плавающей запятой?

Я работаю в JavaScript, но проблема общая. Возьмите эту ошибку округления:

>> 0.1 * 0.2
0.020000000000000004

Этот ответ StackOverflow дает хорошее объяснение. По сути, некоторые десятичные числа не могут быть точно представлены в двоичном виде. Это интуитивно понятно, так как 1/3 имеет аналогичную проблему с основанием 10. Теперь обходной путь таков:

>> (0.1 * (1000*0.2)) / 1000
0.02

Мой вопрос: как это работает?


person gwg    schedule 11.06.2014    source источник
comment
Интересный вопрос, думаю, ответ должен сводиться к тому, где теряется точность.   -  person Ian    schedule 12.06.2014
comment
Последнее может быть просто записано как 0.1 * 200 / 1000 или 20 / 1000, что, конечно, не имеет проблемы с плавающей запятой.   -  person adeneo    schedule 12.06.2014
comment
@аденео, конечно? Объяснять.   -  person Niet the Dark Absol    schedule 12.06.2014
comment
@adeneo: Возможно, вам следует прочитать О чем должен знать каждый программист Арифметика с плавающей запятой   -  person tmyklebu    schedule 12.06.2014
comment
@tmyklebu - я опубликовал это 3 часа назад и прочитал.   -  person adeneo    schedule 12.06.2014
comment
@adeneo: это 0.1 * 200 == 20 нетривиально и фундаментальным образом зависит от двоичного расширения 0.1. Сравните, например, с 0.145 * 200 < 29. Это не заслуживает того, чтобы заметать под ковер одним лишь словом «или».   -  person tmyklebu    schedule 12.06.2014
comment
@tmyklebu - на самом деле это зависит от маскировки (защиты) и от того, как они применяются, когда одна сторона выражения является целым числом.   -  person adeneo    schedule 12.06.2014
comment
@adeneo: я не верю, что это так. Во-первых, числа в Javascript всегда представляют собой 64-битные числа с плавающей запятой. Во-вторых, я не вижу, как согласовать результат 0.145 * 200 со схемой, которую вы, кажется, предлагаете. Возможно, вам стоит еще раз взглянуть на WECSSKAFPA. .   -  person tmyklebu    schedule 12.06.2014
comment
@tmyklebu - то, во что я верю, на самом деле не имеет значения, я не ответил на вопрос, а вы ответили двумя строчками, которые на самом деле ничего не говорят. Попробуйте объяснить мне это, опубликовав правильный ответ, который объясняет это поведение, чтобы каждый мог его понять, и я уверен, что вы получите еще несколько голосов. Это время было бы лучше потратить, чем спорить со мной о том, как работают числа с плавающей запятой.   -  person adeneo    schedule 12.06.2014
comment
И что гораздо более интересно, чем тот факт, что числа являются 64-битными, так это то, что они хранятся в 53-битной мантиссе, как только вы поймете, как это работает, станет очевидным, почему побитовое преобразование не соответствует определенным числам с плавающей запятой.   -  person adeneo    schedule 12.06.2014
comment
@adeneo: меня не волнуют голоса. Вы запутались в арифметике с плавающей запятой и написали запутанный комментарий. Прочитайте документ, на который вы ссылаетесь, и проясните ситуацию в своей голове.   -  person tmyklebu    schedule 12.06.2014
comment
@tmyklebu - Опять же, если вы думаете, что хорошо понимаете, почему это происходит, опубликуйте ответ, объясняющий такое поведение, а не одну строку, в которой говорится, что если вы попробуете другие числа, станет хуже, опубликуйте что-нибудь более существенное и более понятное. .   -  person adeneo    schedule 12.06.2014


Ответы (3)


Это не работает. То, что вы там видите, не точно 0.02, а число, достаточно близкое (до 15 значащих десятичных цифр), чтобы оно выглядело так.

Просто случается, что умножение операнда на 1000, а затем деление результата на 1000 приводит к ошибкам округления, которые дают явно «правильный» результат.

Вы можете сами увидеть эффект в консоли вашего браузера. Преобразуйте числа в двоичные с помощью Number.toString(2), и вы увидите разницу:

Консоль показывает ‹code›0.1‹/code›, ‹code›0.2‹/code›, ‹code›0.1*0.2‹/  code› и ‹code›((0,1*(0,2*1000))/1000‹/code› каждый со своим двоичным представлением

Корреляция не подразумевает причинно-следственной связи.

person Niet the Dark Absol    schedule 11.06.2014

Это не так. Вместо этого попробуйте 0.684 и 0.03, и этот трюк только усугубит ситуацию. Или 0.22 и 0.99. Или огромное количество других вещей.

person tmyklebu    schedule 11.06.2014
comment
Меня не удивляет, что это не всегда работает. Я спрашиваю, почему это вообще работает. Например, (0.22 * (10 * 0.99)) / 10 подходит, а (0.22 * (100 * 0.99)) / 100 — нет. - person gwg; 12.06.2014
comment
@ggundersen: Каждый из этих хаков дает примерно одинаковую вероятность желаемого ответа. Как и наивная формула. Это чистое совпадение. В частности, этот хак не лучше, чем просто умножение двух. - person tmyklebu; 12.06.2014
comment
@ggundersen Почти все, что меняет точную последовательность операндов и операций, может изменить результат. Иногда новый результат будет вам больше нравиться, иногда — не так, как оригинал. - person Patricia Shanahan; 12.06.2014
comment
@ggundersen Вам повезло, и ваши ошибки округления компенсируются, а не суммируются. - person Kendall Frey; 12.06.2014

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

person Aefix    schedule 11.06.2014
comment
Присвоение значений переменным и последующая работа с переменными также дает точный результат. - person Niet the Dark Absol; 12.06.2014
comment
В этом случае вполне возможно, что числа, используемые для представления дроби в числе с плавающей запятой, действительно могли сделать это точно, поэтому потери точности не было. - person Aefix; 12.06.2014