с плавающей запятой — что это за «ненормальные данные»; около ?

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

Кто-то может расшифровать эти 2 слова для меня?

РЕДАКТИРОВАТЬ

пожалуйста, помните, что я ориентирован на приложения C ++ и только язык C ++.

17

Решение

Вы спрашиваете о C ++, но специфика значений и кодировок с плавающей точкой определяется спецификацией с плавающей точкой, в частности IEEE 754, а не C ++. IEEE 754, безусловно, является наиболее широко используемой спецификацией с плавающей запятой, и я отвечу, используя ее.

В IEEE 754 двоичные значения с плавающей точкой кодируются тремя частями: знаковый бит s (0 для положительного, 1 для отрицательного), смещенный показатель е (представленный показатель плюс фиксированное смещение) и поле значений и е (дробная часть). Для нормальных чисел они представляют собой точно число (-1)s • 2есмещение • 1.е, где 1.е это двоичное число, образованное записью значащих и битов после «1». (Например, если поле значим и имеет десять битов 0010111011, оно представляет значение и 1.00101110112, что составляет 1.182617175 или 1211/1024.)

Смещение зависит от формата с плавающей точкой. Для 64-разрядного двоичного кода IEEE 754 поле экспоненты имеет 11 битов, а смещение равно 1023. Когда фактический показатель степени равен 0, поле закодированного показателя равно 1023. Фактические показатели степени -2, -1, 0, 1 и 2 имеют закодированные показатели степени 1021, 1022, 1023, 1024 и 1025. Когда кто-то говорит, что показатель степени субнормального числа равен нулю, это означает, что кодированный показатель степени равен нулю. Фактический показатель будет меньше -1022. Для 64-разрядных нормальный интервал экспоненты составляет от -1022 до 1023 (закодированные значения от 1 до 2046). Когда показатель выходит за пределы этого интервала, происходят особые вещи.

Выше этого интервала с плавающей точкой останавливается представление конечных чисел. Закодированный показатель степени 2047 (все 1 бит) представляет бесконечность (с значением и полем, установленным в ноль). Ниже этого диапазона с плавающей точкой меняется на ненормальные числа. Когда закодированный показатель степени равен нулю, поле Значение и представляет 0.е вместо 1.е.

Для этого есть важная причина. Если бы наименьшее значение показателя степени было просто другим нормальным кодированием, то младшие биты его значения были бы слишком малы, чтобы представлять их в виде значений с плавающей запятой. Без этого первого «1» невозможно было бы сказать, где находится первый 1 бит. Например, предположим, что у вас есть два числа, как с наименьшим показателем, так и с значением 1.00101110112 и 1.00000000002. Когда вы вычитаете значения, результат будет .00101110112. К сожалению, нет способа представить это как нормальное число. Поскольку у вас уже был наименьший показатель степени, вы не можете представить меньший показатель, который необходим, чтобы сказать, где в этом результате находится первая 1. Поскольку математический результат слишком мал, чтобы быть представленным, компьютер будет вынужден вернуть ближайшее представимое число, которое будет равно нулю.

Это создает нежелательное свойство в системе с плавающей запятой, которое вы можете иметь a != b но a-b == 0, Чтобы избежать этого, используются субнормальные числа. Используя субнормальные числа, у нас есть специальный интервал, в котором фактическая экспонента не уменьшается, и мы можем выполнять арифметику, не создавая числа, слишком малые для представления. Когда закодированный показатель степени равен нулю, фактический показатель степени совпадает с тем, когда закодированный показатель степени равен единице, но значение значимого изменяется на 0.е вместо 1.е. Когда мы делаем это, a != b гарантирует, что вычисленная стоимость a-b не ноль.

Вот комбинации значений в кодировках двоичного двоичного числа с плавающей точкой IEEE 754:

Знак экспоненты (е) Значимые и биты (е)        Имея в виду
0 0 0 + ноль
0 0 ненулевой +2-1022• 0.е (Субнормальная)
0 1 до 2046 Все, что угодно +2е-+1023• 1.е (нормальный)
0 2047 0 + бесконечность
0 2047 Ненулевой, но высокий бит выключен +, сигнализирует NaN
0 2047 Высокий бит +, тихий NaN
1 0 0 - ноль
1 0 ненулевой -2-1022• 0.е (Субнормальная)
1 1 до 2046 Все что угодно -2е-+1023• 1.е (нормальный)
1 2047 0-бесконечность
1 2047 Ненулевой, но высокий бит выключен -, сигнализирует NaN
1 2047 High Bit on -, тихий NaN

Некоторые заметки:

+0 и -0 математически равны, но знак сохраняется. Тщательно написанные приложения могут использовать его в определенных особых ситуациях.

NaN означает «не число». Как правило, это означает, что произошел некоторый нематематический результат или произошла другая ошибка, и расчет должен быть отброшен или выполнен другим способом. Как правило, операция с NaN производит другой NaN, таким образом сохраняя информацию о том, что что-то пошло не так. Например, 3 + NaN производит NaN. Сигнальный NaN предназначен для того, чтобы вызвать исключение, либо для указания того, что программа пошла не так, либо для разрешения другому программному обеспечению (например, отладчику) выполнить какое-либо специальное действие. Тихий NaN предназначен для распространения до дальнейших результатов, позволяя завершить оставшуюся часть большого вычисления в тех случаях, когда NaN является лишь частью большого набора данных и будет обрабатываться отдельно позже или будет отброшен.

Знаки + и — сохраняются с NaN, но не имеют математического значения.

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

К сожалению, некоторые процессоры ломаются из-за того, что они либо нарушают стандарт IEEE 754, заменяя субнормальные числа на ноль, либо работают очень медленно при использовании субнормальных чисел. При программировании для таких процессоров вы можете избегать использования ненормальных чисел.

24

Другие решения

Чтобы понять ненормальные значения с плавающей точкой, вы должны сначала понять нормальные. У значения с плавающей запятой есть мантисса и показатель степени. В десятичном значении, например 1.2345E6, 1.2345 — это мантисса, 6 — показатель степени. Хорошая вещь в нотации с плавающей запятой в том, что вы всегда можете написать ее нормализованной Как 0.012345E8 и 0.12345E7 — это то же значение, что и 1.2345E6. Или, другими словами, вы всегда можете сделать первую цифру мантиссы ненулевым числом, если значение не равно нулю.

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

Это очень привлекательная цель оптимизации. Поскольку значение всегда начинается с 1, нет смысла хранить это 1. Что приятно, так это то, что вы получаете дополнительную точность бесплатно. В 64-битном двойном хранилище имеет 52 бита. Фактическая точность составляет 53 бита благодаря подразумеваемой 1.

Мы должны поговорить о наименьшем возможном значении с плавающей запятой, которое вы можете сохранить таким образом. Делая это сначала в десятичном виде, если у вас был десятичный процессор с 5 цифрами памяти в мантиссе и 2 в показателе степени, то наименьшее значение, которое он может хранить, но не ноль, это 1.00000E-99. 1 означает подразумеваемую цифру, которая не сохраняется (не работает в десятичном формате, но терпит меня). Таким образом, мантисса хранит 00000, а экспонента — 99. Вы не можете хранить меньшее число, показатель степени максимален при -99.

Ну, ты можешь. Вы можете отказаться от нормализованного представления и забыть о подразумеваемой оптимизации цифр. Вы можете хранить это де-нормализованы. Теперь вы можете хранить 0.1000E-99 или 1.000E-100. Вплоть до 0,0001E-99 или 1E-103, абсолютное наименьшее число, которое вы можете сохранить.

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

Однако есть и большая проблема: вы теряете точность с ненормализованными числами. Точность вычислений с плавающей запятой ограничена количеством цифр, которые вы можете сохранить. Он интуитивно понятен с поддельным десятичным процессором, который я использовал в качестве примера, он может вычислять только 5 значащих цифр. Пока значение нормализовано, вы всегда получаете 5 значащих цифр.

Но вы потеряете цифры, когда вы нормализуете. Любое значение от 0,1000E-99 до 0,9999E-99 имеет только 4 значащих цифры. Любое значение между 0,0100E-99 и 0,0999E-99 имеет только 3 значащие цифры. Вплоть до 0,0001E-99 и 0,0009E-99 осталась только одна значащая цифра.

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

У процессоров с плавающей запятой есть способы сообщить вам об этом или иным образом обойти проблему. Например, они могут генерировать прерывание или сигнал, когда значение становится ненормализованным, позволяя вам прервать вычисление. И у них есть опция «сброс в ноль», бит в слове состояния, который говорит процессору автоматически преобразовывать все ненормальные значения в ноль. Который имеет тенденцию генерировать бесконечности, результат, который говорит вам, что результат является мусором и должен быть отброшен.

6

От Документация IEEE

Если показатель степени равен 0, но дробь не равна нулю (иначе
интерпретируется как ноль), то значение является денормализованным числом,
который не имеет предполагаемого ведущего 1 перед двоичной точкой.
Таким образом, это представляет число (-1) s × 0.f × 2-126, где s — это
бит знака, а f — дробь. Для двойной точности, денормализованный
числа имеют вид (-1) s × 0.f × 2-1022. Из этого вы можете
интерпретировать ноль как особый тип денормализованного числа.

3

Основы IEEE 754

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

Давайте сначала сосредоточимся на одинарной (32-битной) точности.

Формат такой:

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

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

введите описание изображения здесь

Источник.

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

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

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

  • показатель степени: 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 именно так!

Runnable C пример

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

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

Это особенно касается моего ноутбука Ubuntu 18.04 amd64.

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

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

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

Это всегда хорошая идея иметь геометрическую интуицию о том, что мы изучаем, так что здесь идет.

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

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

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

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

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

Без субнормалей (гипотетических):

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

С субнормалами:

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

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

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

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

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

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

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

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

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

В этой настройке у нас будет пустой промежуток между 0 а также 2^-127, что не очень элегантно.

Интервал, однако, хорошо заполнен и содержит 2^23 плавает как любой другой.

Реализации

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

TODO: какие-нибудь заметные примеры современного оборудования, у которого нет субнормалей?

TODO: любая реализация позволяет контролировать его во время выполнения?

Субнормалы кажутся менее быстрыми, чем нормальные в некоторых реализациях: Почему изменение от 0,1f до 0 снижает производительность в 10 раз?

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

Вот короткий исполняемый пример: Диапазоны типа данных с плавающей точкой в ​​C?

0
По вопросам рекламы [email protected]