Углубленная точность JavaScript с плавающей запятой

внешний интерфейс JavaScript
Углубленная точность JavaScript с плавающей запятой

предисловие

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

Тип номера

Тип Number в ECMAScript использует стандарт IEEE754 для представления целых чисел и значений с плавающей запятой. Так называемый стандарт IEEE754, полное название стандарта двоичной арифметики с плавающей запятой IEEE, этот стандарт определяет формат числа с плавающей запятой и так далее.

В IEEE754 определены четыре способа представления значений с плавающей запятой: с одинарной точностью (32-разрядная), с двойной точностью (64-разрядная), расширенная одинарная точность и расширенная двойная точность. Например, ECMAScript использует двойную точность, то есть использует 64 бита для хранения числа с плавающей запятой.

Плавающая точка в двоичный

Давайте посмотрим на десятичное представление числа 1020:

1020 = 1 * 10^3 + 0 * 10^2 + 2 * 10^1 + 0 * 10^0

Итак, 1020 в десятичном виде равно 1020... (ха-ха)

Что, если число 1020 представить в двоичном формате?

1020 = 1 * 2^9 + 1 * 2^8 + 1 * 2^7 + 1 * 2^6 + 1 * 2^5 + 1 * 2^4 + 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 0 * 2^0

Таким образом, двоичный код 10201111111100

Что, если бы это было 0,75 в двоичном формате? Так же должно быть:

0.75 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...

Поскольку используется двоичный код, значение abcd... равно 0 или 1.

Тогда как вычислить значение abcd..., мы можем умножить обе части на 2 и вычислить его.Решение выглядит следующим образом:

0.75 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4...

Умножьте обе части на 2

1 + 0,5 = a * 2^0 + b * 2^-1 + c * 2^-2 + d * 2^-3... (так что a = 1)

остаток:

0.5 = b * 2^-1 + c * 2^-2 + d * 2^-3...

Умножить на 2 одновременно

1 + 0 = b * 2^0 + c * 2^-2 + d * 2^-3... (таким образом, b = 1)

Таким образом, 0,75 в двоичном формате равно 0,ab, то есть 0,11.

Однако не все числа так легко вычислить, как 0,75.Рассчитаем 0,1:

0.1 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...

0 + 0.2 = a * 2^0 + b * 2^-1 + c * 2^-2 + ...   (a = 0)
0 + 0.4 = b * 2^0 + c * 2^-1 + d * 2^-2 + ...   (b = 0)
0 + 0.8 = c * 2^0 + d * 2^-1 + e * 2^-2 + ...   (c = 0)
1 + 0.6 = d * 2^0 + e * 2^-1 + f * 2^-2 + ...   (d = 1)
1 + 0.2 = e * 2^0 + f * 2^-1 + g * 2^-2 + ...   (e = 1)
0 + 0.4 = f * 2^0 + g * 2^-1 + h * 2^-2 + ...   (f = 0)
0 + 0.8 = g * 2^0 + h * 2^-1 + i * 2^-2 + ...   (g = 0)
1 + 0.6 = h * 2^0 + i * 2^-1 + j * 2^-2 + ...   (h = 1)
....

Затем вы обнаружите, что этот расчет находится в постоянном цикле, поэтому 0,1 использует двоичное значение 0,00011001100110011 ......

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

Хотя 0,1 является числом бесконечного цикла при преобразовании в двоичное число, компьютер всегда должен хранить его. Мы знаем, что ECMAScript использует 64 бита для хранения числа с плавающей запятой. Как оно хранится? Это восходит к стандарту IEEE754, который, в конце концов, определяет метод хранения.

Стандарт говорит, что число с плавающей запятой (значение) может быть представлено следующим образом:

Value = sign * exponent * fraction

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

Например, -1020, выраженное в экспоненциальном представлении:

-1 * 10^3 * 1.02

знак -1, экспозиция 10^3, дробь 1,02

То же самое верно для двоичного кода, 0,00011001100110011 в двоичном формате 0,1... Это число:

Это может быть выражено как:

1*2^-4*1.1001100110011…

где знак равен 1, показатель степени равен 2^-4, дробь равна 1,1001100110011…

Когда используется только представление бинарной экспоненциальной записи, представление этого значения может быть более конкретным, например:

V = (-1)^S * (1 + Fraction) * 2^E

(Если все числа с плавающей запятой можно представить так, то мы сохраняем часть значений, которые изменятся при их сохранении)

Давайте немного посмотрим:

(-1)^SУказывает бит знака, когда S = 0, V положительное, когда S = 1, V отрицательное.

посмотри снова(1 + Fraction), это потому, что все числа с плавающей запятой могут быть выражены в виде 1.xxxx * 2^xxx, первое должно быть 1.xxx, тогда мы просто не храним эту 1, а храним напрямую последнюю xxxxx Ну, это также часть дроби.

см. последний2^E

Если это 1020,75, соответствующее двоичное число равно 1111111100,11, соответствующий двоичный метод научной записи равен 1 * 1,11111110011 * 2 ^ 9, значение E равно 9, а если оно равно 0,1, соответствующее двоичное число равно 1 * 1,1001100110011.. * 2^-4, E Значение равно -4, то есть Е может быть как отрицательным, так и положительным, тогда возникает вопрос, как нам хранить это Е?

Мы решаем это, если мы используем 8 бит для хранения числа E, если есть только положительные числа, диапазон сохраняемых значений составляет 0 ~ 254, и если мы хотим хранить положительные и отрицательные числа, диапазон значений составляет -127~127, мы при сохранении добавляем 127 к числу, которое нужно сохранить, поэтому, когда мы сохраняем -127, мы сохраняем 0, а когда мы сохраняем 127, мы сохраняем 254, что решает проблему хранения отрицательных чисел. Соответственно при взятии значения мы вычитаем 127.

Итак, когда дело доходит до фактического хранения, мы не будем хранить E напрямую, а будем хранить смещение E +.При использовании 8 бит это смещение равно 127.

Итак, если мы хотим хранить число с плавающей запятой, мы можем хранить три значения S, Fraction и E +bias, Сколько бит нужно выделить для хранения этих чисел? IEEE754 дает стандарт:

IEEE754

По этому стандарту:

Мы будем хранить S в 1 бите, 0 для положительных чисел и 1 для отрицательных чисел.

E + смещение хранится в битах 11. Для битов 11 значение смещения равно 2 ^ (11-1) - 1, что составляет 1023.

Дробь хранится в 52 битах.

Например, возьмем 0,1 в качестве примера, соответствующий двоичный код равен 1 * 1,1001100110011... * 2^-4, знак равен 0, E + смещение равно -4 + 1023 = 1019, 1019 в двоичном формате равно 1111111011, дробь равна 1001100110011. .. …

Полное представление для 64 бит:

0 01111111011 1001100110011001100110011001100110011001100110011010

Точно так же полное представление 0,2:

0 01111111100 1001100110011001100110011001100110011001100110011010

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

Операции над числами с плавающей запятой

Работа с числами с плавающей запятой обычно завершается следующими пятью шагами: упорядочение, операция мантиссы, нормализация, округление и оценка переполнения. Кратко рассмотрим расчеты для 0,1 и 0,2.

Во-первых, это порядок сопоставления. Так называемый порядок сопоставления заключается в корректировке кода заказа, чтобы он был одинаковым. Например, 0,11.1001100110011…… * 2^-4, показатель степени равен -4, а 0,21.10011001100110...* 2^-3, код заказа -3, два кода заказа разные, поэтому сначала настройте один и тот же код заказа, а затем выполните расчет.Принцип корректировки - это малый заказ для большого заказа, то есть -4 от 0,1 корректируется до -3, и соответствующее становится0.11001100110011…… * 2^-3

Далее идет расчет мантиссы:

  0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
 10.0110011001100110011001100110011001100110011001100111

Получаем результат как10.0110011001100110011001100110011001100110011001100111 * 2^-3

Обработать этот результат, то есть нормализовать результат, и стать1.0011001100110011001100110011001100110011001100110011(1) * 2^-2

1 в круглых скобках означает, что 1 выходит за пределы диапазона после расчета, поэтому он будет отброшен.

Затем идет округление, округление соответствует двоичному, то есть 0 округляется до 1, потому что мы хотим потерять 1 в скобках, поэтому мы добавим сюда единицу, и результат станет

1.0011001100110011001100110011001100110011001100110100 * 2^-2

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

Таким образом, окончательный результат, хранящийся в 64 битах, равен

0 01111111101 0011001100110011001100110011001100110011001100110100

Преобразуйте его в основание 10, чтобы получить0.30000000000000004440892098500626

Из-за потери точности в двух хранилищах плюс потеря точности в одной операции результат равен 0,1 + 0,2 !== 0,3.

разное

// 十进制转二进制
parseFloat(0.1).toString(2);
=> "0.0001100110011001100110011001100110011001100110011001101"

// 二进制转十进制
parseInt(1100100,2)
=> 100

// 以指定的精度返回该数值对象的字符串表示
(0.1 + 0.2).toPrecision(21)
=> "0.300000000000000044409"
(0.3).toPrecision(21)
=> "0.299999999999999988898"

Ссылаться на

  1. why is 0.1+0.2 not equal to 0.3 in most programming languages
  2. Стандарт IEEE-754 и арифметика с плавающей запятой

Подробная серия

Адрес каталога подробной серии JavaScript:GitHub.com/ в настоящее время имеет бриз…

Ожидается, что в серии углубленных исследований JavaScript будет написано около пятнадцати статей, призванных помочь вам усовершенствовать базовые знания JavaScript, уделяя особое внимание таким аспектам, как прототип, область действия, контекст выполнения, переменный объект, this, закрытие, передача по значению, вызов, применение. , bind, new, наследование и другие сложные понятия.

Если есть какие-либо ошибки или неточности, пожалуйста, поправьте меня, большое спасибо. Если вам нравится или у вас есть вдохновение, добро пожаловать в звезду, что также является поощрением для автора.