Объяснение Леймана, почему JavaScript имеет странную плавающую математику - стандарт IEEE 754

Я никогда не понимаю точно, что происходит с JavaScript, когда я выполняю математические операции над числами с плавающей запятой. Я боялся использовать десятичные дроби до такой степени, что просто избегаю их, когда это возможно. Однако, если бы я знал, что происходит за кулисами, когда дело доходит до стандарта IEEE 754, тогда я мог бы предсказать, что произойдет; с предсказуемостью я буду более уверенным и менее боязливым.

Может ли кто-нибудь дать мне простое объяснение (стольже простое, как объяснение двоичных представлений целых чисел относительно того, как работает стандарт IEEE 754 и как он дает этот побочный эффект: 0.1 + 0.2 != 0.3?

Спасибо!:)

5 ответов

Решение

Десятичные дроби, такие как 0,1, не могут быть четко выражены в базе 2

Допустим, мы хотим выразить десятичную 0,1 в base-2. Мы знаем, что оно равно 1/10. Результат 1, деленный на 10 в базе-2: 0.000110011001100... с повторяющейся последовательностью десятичных дробей.

Таким образом, хотя в десятичной форме на самом деле очень просто представить число, например 0,1, в base-2 вы не можете выразить рациональное число, точно основанное на десятых. Вы можете только приблизить его, используя столько битов, сколько сможете сохранить.

Скажем для упрощения, что у нас достаточно места для хранения, чтобы воспроизвести первые, скажем, 8 значащих двоичных цифр этого числа. Сохраненные цифры будут 11001100 (вместе с показателем степени 11). Это переводит обратно к 0.000110011 в base-2, который в десятичном виде равен 0.099609375, а не 0.1. Это количество ошибок, которое может произойти, если вы преобразуете 0,1 в теоретическую переменную с плавающей запятой, которая хранит базовые значения в 8 битах (не включая бит знака).

Как переменные с плавающей точкой хранят значения

Стандарт IEEE 754 определяет способ кодирования действительного числа в двоичном виде со знаком и показателем двоичной степени. Экспонента применяется в двоичной области, то есть вы не сдвигаете десятичную точку до преобразования в двоичную, вы делаете это после.

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

Когда ты видишь 0.1 + 0.2 != 0.3, это потому, что вы фактически не выполняете вычисления по 0,1 или 0,2, а по приближению этих чисел в двоичной системе с плавающей запятой только с определенной точностью. При преобразовании результата обратно в десятичную из-за этой ошибки результат не будет точно равен 0,3. Кроме того, результат даже не будет равен двоичному приближению 0,3. Фактическая величина ошибки будет зависеть от размера значения с плавающей запятой и, следовательно, от того, сколько битов точности было использовано.

Как иногда помогает округление, но не в этом случае

В некоторых случаях ошибки в вычислениях из-за потери точности при преобразовании в двоичный файл будут достаточно малы, чтобы их можно было округлить до значения во время преобразования обратно из двоичного кода, и поэтому вы никогда не заметите никакой разницы - это будет выглядеть так работал.

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

Однако при 0,1 + 0,2 против 0,3 округление не устраняет ошибку. Результат сложения двоичных приближений 0,1 и 0,2 будет отличаться от двоичного приближения 0,3.

Это та же самая причина, по которой 1/3 + 1/3 + 1/3!= 1, если вы наивно конвертируете 1/3 в 0,333 (или любое конечное число из 3). 0,333 + 0,333 + 0,333 = 0,999, а не 1.

В базе 9 (например) 1/3 можно представить точно как 0,3 9, а 0,3 9 + 0,3 9 + 0,3 9 = 1,0 9. Некоторые числа, которые могут быть представлены точно в базе 9, не могут быть точно представлены в базе 10 и должны обязательно быть округлены до числа, которое может.

Точно так же некоторые числа не могут быть представлены точно в базе 2, но могут быть представлены в базе 10, такой как 0.2.
0,2 10 - 0,0011001100110011... 2
Если это округлено до 0,0011 2, то:
0,0011 2 + 0,0011 2 + 0,0011 2 + 0,0011 2 + 0,0011 2 = 0,1111 2, а не 1,0000 2. (0,1111 2 - 15/16)

Так как компьютеры (по крайней мере те, которые мы используем) выполняют арифметику в двоичном формате, это влияет на них.

Обратите внимание, что точность результата увеличивается, когда мы используем больше цифр. (0,33333333 10 + 0,33333333 10 + 0,33333333 10 = 0,999999999 10, что ближе к правильному ответу, чем 0,9999)
По этой причине погрешность округления обычно очень мала. double хранит около 15 десятичных цифр, поэтому относительная ошибка составляет около 10 -15 (точнее, 2 -52).

Поскольку ошибка мала, обычно это не имеет значения, если только:

  • Ваша программа требует очень высокой точности, или
  • Вы отображаете его с большим количеством десятичных знаков (вы можете увидеть число, например 0,9999999999999995622), или
  • Вы сравниваете два числа на равенство (используя == или же !=).

Сравнение нецелых чисел на равенство определенно стоит избегать, но вы можете использовать их в расчетах и ​​других сравнениях (< или же >) без проблем (опять же, если ваша программа не требует очень высокой точности).

В JavaScript я всегда делаю что-то вроде (Math.abs(.1+.2-.3)<. 000001)

Я всегда думаю, как это... .25 пиццы + .25 пиццы!= .5 пиццы (вы теряете пиццу, когда вы режете) LOL

Если вы хотите уверенности в использовании чисел с плавающей запятой, просто помните, что они хороши как минимум до 15 значащих цифр, что почти всегда достаточно для обычных задач.

Количество значащих цифр, необходимых для повседневной работы, варьируется, например, инженеры могут использовать только 3, экономисты могут использовать 5, ученые могут использовать больше (или меньше). Поэтому сначала определите количество значащих цифр, которые вы хотите (например, хотите ли вы видеть 2345 876 ​​234 долларов или 2,3 миллиарда долларов в порядке). Если оно меньше, чем, скажем, 5 значащих цифр, вы можете безопасно выполнить арифметику, по крайней мере, с 7 значащими цифрами и округлить результат до требуемого числа значащих цифр в самом конце.

например, если вам нужно только 3 значащих цифры:

(0.1 + 0.2).toFixed(3) // 0.300

Если вы всегда работаете как минимум с двумя значащими цифрами, чем вам нужно, то округлите в конце нужное число, и вас не будут беспокоить крошечные ошибки, представленные числами JavaScript.

Это объясняется по следующей ссылке Ошибка представления с плавающей точкой

Другие вопросы по тегам