Статьи

Как числа кодируются в JavaScript


Все числа в JavaScript с плавающей точкой.
В этом блоге объясняется, как эти числа с плавающей запятой представляются внутри в 64-битном двоичном формате Особое внимание будет уделено целым числам, так что после прочтения этого поста вы поймете, что происходит в следующем взаимодействии:

    > 9007199254740992 + 1
    9007199254740992

    > 9007199254740992 + 2
    9007199254740994

Номера JavaScript

Все числа JavaScript являются числами с плавающей запятой и хранятся в соответствии со
стандартом IEEE 754 . Этот стандарт имеет несколько форматов. JavaScript использует
двоичную64 или
двойную точность . Как указывает прежнее имя, числа хранятся в двоичном формате в 64 битах. Эти биты распределяются следующим образом:
дробь занимает биты от 0 до 51,
показатель степени занимает биты от 52 до 62,
знак занимает бит 63.

 

знак
(1 бит)


63
показатель степени
(11 бит)


62

52
дробь
(52 бита)


51

0

Компоненты работают следующим образом: если бит знака равен 0, число является положительным, в противном случае — отрицательным. Грубо говоря, дробь содержит цифры числа, а показатель степени указывает, где находится точка. В дальнейшем мы будем часто использовать двоичные числа, что немного необычно, когда дело доходит до числа с плавающей запятой. Двоичные числа будут отмечены префиксным знаком процента (%). В то время как числа JavaScript хранятся в двоичном формате, вывод по умолчанию является десятичным [1] . В примерах мы обычно будем работать с этим значением по умолчанию.

Фракция

Ниже приведен один из способов представления неотрицательных чисел с плавающей запятой:
Значениеи (или
мантисса ) содержит цифры, как натуральное число, показатель степени указывает, сколько цифр слева (отрицательный показатель) или справа (положительный показатель) точки должен быть сдвинут. В числах JavaScript в качестве значения используется рациональное число: 1.
f, где
f — это 52-битная дробь. Игнорируя знак, число — это значение, умноженное на 2
p, где
p — показатель степени (после преобразования, которое будет объяснено позже).

Примеры:

f =% 101, p = 2 Число:% 1,101 × 2 2 =% 110,1
f =% 101, p = -2 Число:% 1,101 × 2 -2 =% 0,01101
f = 0, p = 0 Число:% 1,0 × 2 0 =% 1

Представление целых чисел

Сколько битов дает вам кодировка для целых чисел? Имеет 53 цифры, одна до точки, 52 после точки. С
p = 52 у нас есть 53-битное натуральное число. Единственная проблема состоит в том, что старший бит всегда равен 1. То есть, мы не имеем в своем распоряжении всех битов свободно. Один снимает это ограничение в два этапа. Во-первых, если вам нужно 53-разрядное число, старший бит которого равен 0, а затем 1, вы устанавливаете
p = 51. Самый младший бит дроби становится первой цифрой после точки и равен 0 для целых чисел. И так до тех пор, пока вы не достигнете
p = 0 и
f = 0, что кодирует число 1.

 

  52 51 50 1 0 (биты)
р = 52 1 F 51 ф 50 f 1 f 0  
р = 51 0 1 F 51 f 2 f 1 f 0 = 0
 
р = 0 0 0 0 0 1 f 51 = 0 и т. д.

Во-вторых, для полных 53 битов мы все равно должны представлять ноль. Как это сделать, объясняется в следующем разделе. Обратите внимание, что у нас есть полные 53 бита для величины (абсолютного значения) целого числа, поскольку знак хранится отдельно.

Экспонента

Показатель имеет длину 11 бит, что означает, что его самое низкое значение равно 0, а самое высокое значение равно 2047 (2
11 -1). Для поддержки отрицательных показателей используется так называемое
двоичное кодирование смещения : 1023 — ноль, все младшие числа отрицательны, все старшие числа положительны. Это означает, что вы вычитаете 1023 из показателя степени, чтобы преобразовать его в нормальное число. Следовательно, переменная
p, которую мы ранее использовали, равна
e -1023, а значение и умножается на 2
e -1023 .

Несколько чисел в двоичной кодировке смещения:

    %00000000000     0  ?  -1023  (lowest number)
    %01111111111  1023  ?      0
    %11111111111  2047  ?   1024  (highest number)
                         
    %10000000000  1024  ?      1
    %01111111110  1022  ?     -1 

Чтобы отменить число, вы инвертируете его биты и вычтите 1.

 

Специальные показатели

Зарезервированы два значения показателя: самое низкое (0) и самое высокое (2047). Показатель 2047 используется для значений бесконечности и NaN (не числа)
[2] . Стандарт IEEE 754 имеет много значений NaN, но JavaScript все представляет их как одно значение NaN. Показатель 0 используется в двух значениях. Во-первых, если дробь также равна 0, тогда целое число равно 0. Поскольку знак хранится отдельно, у нас есть и -0, и +0 (подробности см. В
[3] ).

Во-вторых, показатель степени 0 также используется для представления очень маленьких чисел (близких к нулю). Тогда дробь должна быть ненулевой, и, если она положительна, число вычисляется через


% 0.
f × 2
-1022

Это представление называется
денормализованным . Ранее обсужденное представление называется
нормализованным . Наименьшее положительное (ненулевое) число, которое может быть представлено в нормализованном виде:


% 1,0 × 2
-1022

Наибольшее денормализованное число


% 0,1 × 2
-1022

Таким образом, при переключении между нормированными и денормализованными числами нет дыры.

Резюме: показатели

(-1) с ×% 1. f × 2 e -1023 нормализованный, 0 < e <2047
(-1) с ×% 0. f × 2 e -1022 денормализованный, e = 0, f > 0
(-1) с × 0 е = 0, е = 0
NaN е = 2047, е > 0
(-1) с × 8 (бесконечность) е = 2047, е = 0

При p = e — 1023 показатель степени имеет диапазон


-1023 <
р <1024

Десятичные дроби

Не все десятичные дроби могут быть представлены точно в JavaScript, как показано следующим результатом:

    > 0.1 + 0.2
    0.30000000000000004

Ни одна из десятичных дробей 0,1 и 0,2 не может быть представлена ​​точно как двоичное число с плавающей запятой. Тем не менее, отклонение от фактического значения обычно слишком мало для отображения. Дополнение приводит к тому, что это отклонение становится видимым. Другой пример:

    > 0.1 + 1 - 1
    0.10000000000000009

Представление 0,1 равняется задаче представления дроби 110. Сложной частью является знаменатель 10, у которого первичная факторизация равна 2 × 5. Показатель степени позволяет только разделить целое число на степень 2, поэтому нет способа получить 5 дюймов. Сравните: 13 не могут быть представлены точно как десятичная дробь. Это приблизительно 0.333333 …

Напротив, представление двоичной дроби в виде десятичной дроби всегда возможно, вам просто нужно собрать достаточно двойки (из которых каждая десятая имеет одну). Например:


% 0,001 = 18 = 12 × 2 × 2 = 5 × 5 × 5 (2 × 5) × (2 × 5) × (2 × 5) = 12510 × 10 × 10 = 0,125

Сравнение десятичных дробей

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

    var epsEqu = function () { // IIFE, keeps EPSILON private
        var EPSILON = Math.pow(2, -53);
        return function epsEqu(x, y) {
            return Math.abs(x - y) < EPSILON;
        };
    }();

Вышеприведенная функция обеспечивает правильные результаты, если нормальное сравнение будет неадекватным:

    > 0.1 + 0.2 === 0.3
    false
    > epsEqu(0.1+0.2, 0.3)
    true

Максимальное целое число

Что значит, если кто-то говорит «
х — максимальное целое число»? Это означает, что каждое целое число
n в диапазоне 0 =
n =
x может быть представлено и что то же самое не выполняется для любого целого числа, большего, чем
x . 2
53 соответствует этому счету. Все предыдущие цифры могут быть представлены:

    > Math.pow(2, 53)
    9007199254740992
    > Math.pow(2, 53) - 1
    9007199254740991
    > Math.pow(2, 53) - 2
    9007199254740990

Но следующее целое число не может быть представлено:

    > Math.pow(2, 53) + 1
    9007199254740992

Несколько аспектов 2
53, являющихся верхним пределом, могут быть удивительными. Мы рассмотрим их через ряд вопросов. Следует иметь в виду, что ограничивающим ресурсом в верхнем конце целочисленного диапазона является дробь; экспоненту еще есть куда расти.

Почему 53 бита? Для величины доступно 53 бита (исключая знак), но дробь составляет всего 52 бита. Как это возможно? Как вы видели выше, показатель степени обеспечивает 53-й бит: он сдвигает дробь, так что могут быть представлены все 53-разрядные числа, кроме нуля, и имеет специальное значение для представления нуля (в сочетании с дробью 0).

 

Почему наибольшее целое число не 2 53 -1? Обычно бит x означает, что наименьшее число равно 0, а наибольшее число равно 2 x -1. Например, старшее 8-битное число — 255. В JavaScript самая высокая дробь действительно используется для числа 2 53 -1, но 2 53 можно представить благодаря помощи показателя — это просто дробь f = 0 и показатель степени p = 53 (после преобразования):


% 1.
f × 2
p =% 1,0 × 2
53 = 2
53

Почему могут быть представлены числа выше 2 53 ? Примеры:

    > Math.pow(2, 53)
    9007199254740992
    > Math.pow(2, 53) + 1  // not OK
    9007199254740992
    > Math.pow(2, 53) + 2  // OK
    9007199254740994

    > Math.pow(2, 53) * 2  // OK
    18014398509481984

2
53 × 2 работает, потому что показатель степени может быть использован. Каждое умножение на 2 просто увеличивает показатель степени на 1 и не влияет на дробь. Таким образом, умножение на степень 2 не является проблемой для максимальной доли. Чтобы понять, почему можно добавить от 2 до 2
53 , но не 1, мы расширим предыдущую таблицу дополнительными битами 53 и 54 и строками для
p = 53 и
p = 54:

 

  54 53 52 51 50 2 1 0 (биты)
р = 54 1 F 51 ф 50 ф 49 ф 48 f 0 0 0  
р = 53   1 F 51 ф 50 ф 49 f 1 f 0 0  
р = 52     1 F 51 ф 50 f 2 f 1 f 0  

Глядя на строку ( p = 53), должно быть очевидно, что числа JavaScript могут иметь бит 53, установленный в 1. Но поскольку дробь f имеет только 52 бита, бит 0 должен быть нулевым. Следовательно, только четные числа x могут быть представлены в диапазоне 2 53 = x <2 54 . В строке ( p = 54) это расстояние увеличивается до четырехкратных кратных значений в диапазоне 2 54 = x <2 55 :

    > Math.pow(2, 54)
    18014398509481984
    > Math.pow(2, 54) + 1
    18014398509481984
    > Math.pow(2, 54) + 2
    18014398509481984
    > Math.pow(2, 54) + 3
    18014398509481988
    > Math.pow(2, 54) + 4
    18014398509481988

И так далее…

IEEE 754 исключения

Стандарт IEEE 754 описывает пять
исключений , где невозможно вычислить точное значение:

  1. Invalid: была выполнена недопустимая операция. Например, вычисление квадратного корня из отрицательного числа. Возвращает NaN [2] .
        > Math.sqrt(-1)
        NaN
    
  2. Деление на ноль: возвращает плюс или минус бесконечность [2] .
        > 3 / 0
        Infinity
        > -5 / 0
        -Infinity
    
  3. Переполнение: результат слишком велик для представления. Это означает, что показатель степени слишком высок ( р = 1024). В зависимости от знака, есть положительное и отрицательное переполнение. Возвращает плюс или минус бесконечность.
        > Math.pow(2, 2048)
        Infinity
        > -Math.pow(2, 2048)
        -Infinity
    
  4. Underflow: результат слишком близок к нулю, чтобы быть представленным. Это означает, что показатель степени слишком низок ( p = -1023). Возвращает денормализованное значение или ноль.
        > Math.pow(2, -2048)
        0
    
  5. Неточно. Операция дала неточный результат — слишком много значащих цифр для фракции, которую нужно сохранить. Возвращает округленный результат.
        > 0.1 + 0.2
        0.30000000000000004
        
        > 9007199254740992 + 1
        9007199254740992
    

№ 3 и № 4 о показателе степени, № 5 о доле. Разница между # 3 и # 5 очень тонкая: во втором примере, приведенном для # 5, мы превышаем верхний предел дроби (что было бы переполнением при целочисленных вычислениях). Но только превышение верхнего предела показателя степени в IEEE 754 называется переполнением.

Вывод

В этом посте мы рассмотрели, как JavaScript помещает свои числа с плавающей запятой в 64-битные. Это делается в соответствии с
двойной точностью в стандарте IEEE 754. Из-за того, как отображаются числа, можно забыть, что JavaScript не может точно представлять десятичную дробь, в которой первичная факторизация знаменателя содержит число, отличное от 2. Например, можно представить 0,5 (12), а 0,6 (35) — нет. Также можно забыть, что три компонента знак, экспонента, дробь числа работают вместе, чтобы представить целое число. Но каждый сталкивается с тем фактом, когда Math.pow (2, 53) + 2 может быть представлен, а Math.pow (2, 53) + 1 не может быть представлен.

Бонус: веб-страница « IEEE-754 Analysis » позволяет вам ввести число и посмотреть его внутреннее представление.

Источники и связанные с ними чтения

Источники этого поста:

 

С http://www.2ality.com/2012/04/number-encoding.html