Подтвердить что ты не робот

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

isnormal() ссылается на страницу:

Определяет, является ли заданное число с плавающей запятой нормальным, т.е. Не является ни нулевым, ни субнормальным, ни бесконечным, ни NaN.

Число, равное нулю, бесконечное или NaN, ясно, что это означает. Но это также говорит о ненормальности. Когда число является субнормальным?

4b9b3361

Ответ 1

В стандарте IEEE754 числа с плавающей запятой представлены в виде двоичной научной нотации, x = M & times; 2 e. Здесь M - мантисса, e - показатель степени. Математически вы всегда можете выбрать экспоненту так, чтобы 1 & le; M < 2. * Однако, поскольку в компьютерном представлении экспонента может иметь только конечный диапазон, есть некоторые числа, которые больше нуля, но меньше 1.0 & times; 2 e min. Эти числа являются субнормальными или денормальными.

Практически, мантисса хранится без ведущего 1, так как всегда есть ведущее 1, за исключением субнормальных чисел (и ноль). Таким образом, интерпретация заключается в том, что если показатель не минимален, существует неявное начало 1, а если показатель минимален, то нет и число является субнормальным.

*) В более общем плане, 1 & le; M < B для любой нотации базы-B.

Ответ 2

Основы IEEE 754

Сначала давайте рассмотрим основы IEEE 754 номера организованы.

Мы сосредоточимся на одинарной точности (32-битной), но все можно сразу обобщить на другие точности.

Формат:

  • 1 бит: знак
  • 8 бит: показатель степени
  • 23 бита: дробь

Или, если вам нравятся картинки:

enter image description here

Источник.

Знак прост: 0 положителен, а 1 отрицателен, конец истории.

Экспонента имеет длину 8 битов, поэтому она варьируется от 0 до 255.

Экспонента называется смещенной, потому что она имеет смещение -127, например :

  0 == special case: zero or subnormal, explained below
  1 == 2 ^ -126
    ...
125 == 2 ^ -2
126 == 2 ^ -1
127 == 2 ^  0
128 == 2 ^  1
129 == 2 ^  2
    ...
254 == 2 ^ 127
255 == special case: infinity and NaN

Соглашение о ведущих битах

При разработке IEEE 754 инженеры заметили, что все числа, кроме 0.0, имеют двоичный 1 в качестве первой цифры. E.g.:

25.0   == (binary) 11001 == 1.1001 * 2^4
 0.625 == (binary) 0.101 == 1.01   * 2^-1

оба начинаются с этой раздражающей части 1..

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

По этой причине они создали "ведущее соглашение по битам":

всегда предполагайте, что число начинается с единицы

Но тогда как бороться с 0.0? Ну, они решили создать исключение:

  • если показатель равен 0
  • и фракция равна 0
  • тогда число представляет плюс или минус 0.0

так что байты 00 00 00 00 также представляют 0.0, что выглядит хорошо.

Если бы мы рассматривали только эти правила, то наименьшее ненулевое число, которое можно представить, было бы:

  • экспонента: 0
  • фракция: 1

который выглядит примерно так в шестнадцатеричной дроби из-за соглашения о ведущих битах:

1.000002 * 2 ^ (-127)

где .000002 равен 22 нулям с 1 в конце.

Мы не можем взять fraction = 0, иначе это будет 0.0.

Но затем инженеры, которые также имели острый артистический смысл, подумали: разве это не уродливо? Что мы прыгаем с прямой 0.0 к чему-то, что даже не является степенью 2? Разве мы не можем представить даже меньшие числа?

Субнормальные числа

Инженеры немного почесали головы и, как обычно, вернулись с еще одной хорошей идеей. Что если мы создадим новое правило:

Если показатель равен 0, то:

  • ведущий бит становится 0
  • показатель степени фиксируется на -126 (не -127, как если бы у нас не было этого исключения)

Такие числа называются субнормальными числами (или ненормальными числами, которые являются синонимами).

Это правило сразу подразумевает, что число такое, что:

  • показатель степени: 0
  • фракция: 0

это 0.0, что довольно элегантно, поскольку означает, что нужно следить за одним правилом меньше.

Таким образом, 0.0 на самом деле является ненормальным числом в соответствии с нашим определением!

Тогда с этим новым правилом наименьшее не субнормальное число:

  • показатель степени: 1 (0 будет субнормальным)
  • фракция: 0

который представляет:

1.0 * 2 ^ (-126)

Тогда самое большое субнормальное число:

  • экспонента: 0
  • фракция: 0x7FFFFF (23 бита 1)

что равно:

0.FFFFFE * 2 ^ (-126)

где .FFFFFE еще раз - 23 бита на единицу справа от точки.

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

И наименьшее ненулевое субнормальное число:

  • экспонента: 0
  • фракция: 1

что равно:

0.000002 * 2 ^ (-126)

который также выглядит довольно близко к 0.0!

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

Как видите, субнормальные числа делают компромисс между точностью и длиной представления.

В качестве самого экстремального примера, самое маленькое ненулевое субнормальное:

0.000002 * 2 ^ (-126)

по существу имеет точность одного бита вместо 32-битного. Например, если мы разделим его на два:

0.000002 * 2 ^ (-126) / 2

мы действительно достигли 0.0 точно!

Визуализация

Всегда полезно иметь геометрическую интуицию в отношении того, что мы изучаем, так что здесь.

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

          +---+-------+---------------+-------------------------------+
exponent  |126|  127  |      128      |              129              |
          +---+-------+---------------+-------------------------------+
          |   |       |               |                               |
          v   v       v               v                               v
          -------------------------------------------------------------
floats    ***** * * * *   *   *   *   *       *       *       *       *
          -------------------------------------------------------------
          ^   ^       ^               ^                               ^
          |   |       |               |                               |
          0.5 1.0     2.0             4.0                             8.0

Отсюда видно, что для каждого показателя степени:

  • для каждого показателя степени нет совпадений между представленными числами
  • для каждого показателя у нас есть одно и то же число 2 ^ 32 чисел (здесь представлено 4 *)
  • точки распределены одинаково для данного показателя степени
  • большие показатели охватывают большие диапазоны, но с более широкими точками

Теперь, давайте снизим это до степени 0.

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

          +---+---+-------+---------------+-------------------------------+
exponent  | ? | 0 |   1   |       2       |               3               |
          +---+---+-------+---------------+-------------------------------+
          |   |   |       |               |                               |
          v   v   v       v               v                               v
          -----------------------------------------------------------------
floats    *    **** * * * *   *   *   *   *       *       *       *       *
          -----------------------------------------------------------------
          ^   ^   ^       ^               ^                               ^
          |   |   |       |               |                               |
          0   |   2^-126  2^-125          2^-124                          2^-123
              |
              2^-127

С субнормалами это выглядит так:

          +-------+-------+---------------+-------------------------------+
exponent  |   0   |   1   |       2       |               3               |
          +-------+-------+---------------+-------------------------------+
          |       |       |               |                               |
          v       v       v               v                               v
          -----------------------------------------------------------------
floats    * * * * * * * * *   *   *   *   *       *       *       *       *
          -----------------------------------------------------------------
          ^   ^   ^       ^               ^                               ^
          |   |   |       |               |                               |
          0   |   2^-126  2^-125          2^-124                          2^-123
              |
              2^-127

Сравнивая два графика, мы видим, что:

  • субнормалы удваивают длину диапазона экспоненты 0, от [2^-127, 2^-126) до [0, 2^-126)

    Расстояние между поплавками в субнормальном диапазоне такое же, как и для [0, 2^-126).

  • диапазон [2^-127, 2^-126) имеет половину количества точек, которые он имел бы без субнормалей.

    Половина этих точек идет на заполнение другой половины диапазона.

  • диапазон [0, 2^-127) имеет несколько точек с субнормальными значениями, но ни одна из них не имеет.

    Это отсутствие очков в [0, 2^-127) не очень элегантно и является основной причиной существования субнормалей!

  • поскольку точки расположены на одинаковом расстоянии:

    • диапазон [2^-128, 2^-127) имеет половину точек, чем [2^-127, 2^-126) - [2^-129, 2^-128) имеет половину очков, чем [2^-128, 2^-127)
    • и так далее

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

Пример работоспособного C

Теперь давайте поиграем с реальным кодом, чтобы проверить нашу теорию.

Почти на всех современных и настольных компьютерах C float представляет числа с плавающей точкой IEEE 754 одинарной точности.

В частности, это касается моего ноутбука Ubuntu 18.04 amd64 Lenovo P51.

С этим допущением все утверждения передаются следующей программе:

subnormal.c

#if __STDC_VERSION__ < 201112L
#error C11 required
#endif

#ifndef __STDC_IEC_559__
#error IEEE 754 not implemented
#endif

#include <assert.h>
#include <float.h> /* FLT_HAS_SUBNORM */
#include <inttypes.h>
#include <math.h> /* isnormal */
#include <stdlib.h>
#include <stdio.h>

#if FLT_HAS_SUBNORM != 1
#error float does not have subnormal numbers
#endif

typedef struct {
    uint32_t sign, exponent, fraction;
} Float32;

Float32 float32_from_float(float f) {
    uint32_t bytes;
    Float32 float32;
    bytes = *(uint32_t*)&f;
    float32.fraction = bytes & 0x007FFFFF;
    bytes >>= 23;
    float32.exponent = bytes & 0x000000FF;
    bytes >>= 8;
    float32.sign = bytes & 0x000000001;
    bytes >>= 1;
    return float32;
}

float float_from_bytes(
    uint32_t sign,
    uint32_t exponent,
    uint32_t fraction
) {
    uint32_t bytes;
    bytes = 0;
    bytes |= sign;
    bytes <<= 8;
    bytes |= exponent;
    bytes <<= 23;
    bytes |= fraction;
    return *(float*)&bytes;
}

int float32_equal(
    float f,
    uint32_t sign,
    uint32_t exponent,
    uint32_t fraction
) {
    Float32 float32;
    float32 = float32_from_float(f);
    return
        (float32.sign     == sign) &&
        (float32.exponent == exponent) &&
        (float32.fraction == fraction)
    ;
}

void float32_print(float f) {
    Float32 float32 = float32_from_float(f);
    printf(
        "%" PRIu32 " %" PRIu32 " %" PRIu32 "\n",
        float32.sign, float32.exponent, float32.fraction
    );
}

int main(void) {
    /* Basic examples. */
    assert(float32_equal(0.5f, 0, 126, 0));
    assert(float32_equal(1.0f, 0, 127, 0));
    assert(float32_equal(2.0f, 0, 128, 0));
    assert(isnormal(0.5f));
    assert(isnormal(1.0f));
    assert(isnormal(2.0f));

    /* Quick review of C hex floating point literals. */
    assert(0.5f == 0x1.0p-1f);
    assert(1.0f == 0x1.0p0f);
    assert(2.0f == 0x1.0p1f);

    /* Sign bit. */
    assert(float32_equal(-0.5f, 1, 126, 0));
    assert(float32_equal(-1.0f, 1, 127, 0));
    assert(float32_equal(-2.0f, 1, 128, 0));
    assert(isnormal(-0.5f));
    assert(isnormal(-1.0f));
    assert(isnormal(-2.0f));

    /* The special case of 0.0 and -0.0. */
    assert(float32_equal( 0.0f, 0, 0, 0));
    assert(float32_equal(-0.0f, 1, 0, 0));
    assert(!isnormal( 0.0f));
    assert(!isnormal(-0.0f));
    assert(0.0f == -0.0f);

    /* ANSI C defines FLT_MIN as the smallest non-subnormal number. */
    assert(FLT_MIN == 0x1.0p-126f);
    assert(float32_equal(FLT_MIN, 0, 1, 0));
    assert(isnormal(FLT_MIN));

    /* The largest subnormal number. */
    float largest_subnormal = float_from_bytes(0, 0, 0x7FFFFF);
    assert(largest_subnormal == 0x0.FFFFFEp-126f);
    assert(largest_subnormal < FLT_MIN);
    assert(!isnormal(largest_subnormal));

    /* The smallest non-zero subnormal number. */
    float smallest_subnormal = float_from_bytes(0, 0, 1);
    assert(smallest_subnormal == 0x0.000002p-126f);
    assert(0.0f < smallest_subnormal);
    assert(!isnormal(smallest_subnormal));

    return EXIT_SUCCESS;
}

GitHub upstream.

Скомпилируйте и запустите с:

gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c
./subnormal.out

C++

В дополнение к раскрытию всех API-интерфейсов C, C++ также предоставляет некоторые дополнительные субнормальные связанные функции, которые не так легко доступны в C в <limits>, например :

  • denorm_min: возвращает минимальное положительное субнормальное значение типа T

В C++ API отверстий шаблонируется для каждого типа с плавающей запятой и намного лучше.

Реализации

x86_64 и ARMv8 реализуют IEEE 754 непосредственно на оборудовании, на которое код C переводит.

В некоторых реализациях субнормалы кажутся менее быстрыми, чем нормальные: Почему изменение от 0,1f до 0 снижает производительность в 10 раз? Это упоминается в руководстве по ARM, см. раздел "Подробности ARMv8" этого ответа.

Подробности ARMv8

Справочное руководство по архитектуре ARM ARMv8 DDI 0487C.a руководство A1.5.4 "Сброс в ноль" описывает конфигурируемый режим, в котором субнормалы округляются до нуля для повышения производительности:

Производительность обработки с плавающей запятой может быть снижена при выполнении вычислений, включающих денормализованные числа и исключения Underflow. Во многих алгоритмах эту производительность можно восстановить, не оказывая существенного влияния на точность конечного результата, заменив денормализованные операнды и промежуточные результаты нулями. Чтобы разрешить эту оптимизацию, реализации ARM с плавающей точкой позволяют использовать режим Flush-to-zero для различных форматов с плавающей точкой следующим образом:

  • For AArch64:

    • Если FPCR.FZ==1, то режим Flush-to-Zero используется для всех входов и выходов одинарной и двойной точности всех инструкций.

    • If FPCR.FZ16==1, then Flush-to-Zero mode is used for all Half-Precision inputs и outputs of floating-point instructions, other than:—Conversions between Half-Precision и Single-Precision numbers.—Conversions between Half-Precision и Double-Precision numbers.

A1.5.2 "Стандарты с плавающей точкой и терминология" Таблица A1-3 "Терминология с плавающей точкой" подтверждает, что субнормалы и денормалы являются синонимами:

This manual                 IEEE 754-2008
-------------------------   -------------
[...]
Denormal, or denormalized   Subnormal

C5.2.7 "FPCR, регистр управления с плавающей запятой" описывает, как ARMv8 может необязательно вызывать исключения или устанавливать биты флага, когда вход операции с плавающей запятой является ненормальным:

FPCR.IDE, bit [15] Ввод Денормалированная ловушка исключения с плавающей точкой. Возможные значения:

  • 0b0 Выбрана необработанная обработка исключений. Если возникает исключение с плавающей точкой, то бит FPSR.IDC устанавливается в 1.

  • 0b1 Выбрана обработка захваченных исключений. Если возникает исключение с плавающей точкой, PE не обновляет бит FPSR.IDC. Программное обеспечение для обработки прерываний может решить, следует ли устанавливать бит FPSR.IDC в 1.

D12.2.88 "MVFR1_EL1, AArch32 Media и VFP Feature Register 1" показывает, что ненормальная поддержка на самом деле является полностью необязательной, и предлагает бит для определения, есть ли поддержка:

FPFtZ, bits [3:0]

Переход в нулевой режим. Указывает, обеспечивает ли реализация с плавающей запятой поддержку только для режима работы Flush-to-Zero. Определены значения:

  • 0b0000 Не реализовано, или аппаратное обеспечение поддерживает только режим работы "Flush-to-Zero".

  • 0b0001 Аппаратное обеспечение поддерживает арифметику полного денормализованного числа.

Все остальные значения зарезервированы.

В ARMv8-A разрешенными значениями являются 0b0000 и 0b0001.

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

Бесконечность и NaN

Любопытно? Я написал кое-что по адресу:

Ответ 3

Из http://blogs.oracle.com/d/entry/subnormal_numbers:

Существует потенциально несколько способов представления одного и того же числа, используя десятичное число в качестве примера, число 0,1 может быть представлено как 1 * 10 -1 или 0,1 * 10 0 или даже 0,01 * 10. Стандарт диктует, что числа всегда сохраняются с первым битом как единым. В десятичном значении соответствует примеру 1 * 10-1.

Теперь предположим, что наименьший показатель, который может быть представлен, равен -100. Поэтому наименьшее число, которое может быть представлено в нормальной форме, равно 1 * 10 -100. Однако, если мы расслабляем ограничение, что ведущий бит один, тогда мы можем фактически представлять меньшие числа в одном и том же пространство. Взяв десятичный пример, мы могли бы представить 0,1 * 10 -100. Эта называется субнормальным числом. Назначение субнормальных чисел состоит в том, чтобы сгладить зазор между наименьшим нормальным числом и нулем.

Очень важно понять, что обозначены субнормальные числа с меньшей точностью, чем обычные. Фактически, они торгуют уменьшенная точность их меньшего размера. Следовательно, расчеты, которые используют субнормальные числа не будут иметь такую ​​же точность, как расчеты на нормальные числа. Таким образом, приложение, которое делает значительное вычисление на субнормальных числах, вероятно, стоит исследуя, можно ли перемасштабировать (т.е. умножить числа на некоторый коэффициент масштабирования) приведет к меньшему количеству субнормальных значений и более точному Результаты.