Что такое субнормальное число с плавающей точкой? - PullRequest
60 голосов
/ 01 декабря 2011

страница ссылки isnormal () сообщает:

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

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

Ответы [ 3 ]

59 голосов
/ 01 декабря 2011

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

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

*) В общем, 1 & le; M <<em> B для любой базы - B научная запись.

27 голосов

Основы 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 в двоичном виде

Например:

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, как если бы у нас не было этого исключения)

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

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

  • экспонента: 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

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

Почти на всех современных и настольных компьютерах 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 «Сброс в ноль» описывает настраиваемый режим, в котором субнормалы округляются доноль для улучшения производительности:

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

  • Для AArch64:

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

    • ЕслиFPCR.FZ16==1, затем режим Flush-to-Zero используется для всех входов и выходов Half-Precision команд с плавающей запятой, кроме: - Преобразования между числами Half-Precision и Single-Precision. - Преобразования между Half-Precision и Double-Точности.

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

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

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

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, биты [3: 0]

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

  • 0b0000 Не реализовано, или аппаратное обеспечение поддерживает только режим работы с заполнением нулями.

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

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

В ARMv8-A допустимые значения: 0b0000 и 0b0001.

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

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

Любопытно?Я написал несколько вещей по адресу:

25 голосов
/ 01 декабря 2011

С http://blogs.oracle.com/d/entry/subnormal_numbers:

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

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

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

...