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

Рубрика:  Арифметика с плавающей запятойIEEE-754Основы

На простом примере показана логика Стандарта IEEE-754, используемая для бинарного представления чисел с плавающей запятой.

Все данные в компьютере хранятся в виде последовательности битов, интерпретация которых по заранее оговоренным правилам и даёт нужную информацию. Для того чтобы записать число с плавающей запятой в виде последовательности битов, нужно определиться с правилами такой записи. Наиболее известный и распространённый способ предлагается в Стандарте IEEE-754. В этой статье мы снова вернёмся к нашему примеру, в котором $p=3$, $e_{min}=-1$ и $e_{max}=2$, и рассмотрим способ хранения таких чисел в соответствии с логикой IEEE-754. Разумеется, что в самом Стандарте числа с этими нашими ограничениями не предусмотрены, но мы сейчас ставим своей целью наглядно показать саму логику кодирования, чтобы затем проще было перейти непосредственно к типам данных, описанным в Стандарте.

Во-первых, числа могут быть положительными и отрицательными, а значит нам нужен один бит, отвечающий за знак. Назовём этот бит s. По Стандарту если s=0, то число положительное, а если s=1, то отрицательное. Это удобно, так как знак числа определяется по формуле (−1)s.

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

В-третьих, необходимо сохранить порядок числа. Вот здесь ситуация довольно сложная, поэтому будьте внимательны, некоторые моменты, сказанные здесь, окончательно проясняться позже. Давайте для нашего примера зарезервируем под порядок числа 3 бита. Экспонента может быть положительной и отрицательной, поэтому возникает естественный вопрос: как кодировать отрицательные значения? Можно было бы записать их в дополнительном коде, по аналогии с тем, как это делается для целых чисел, однако есть более удачное решение, причина которого будет ясна позже.

В Стандарте предлагается хранить так называемую смещённую экспоненту $e_b$, которая определяется по формуле $e_b=e+\mathrm{bias}$. Величина $\mathrm{bias}$ выбирается так, чтобы смещённая экспонента $e_b$ всегда была строго положительной. Далее, как уже было сказано ранее, Стандарт определяет свои типы данных таким образом, чтобы выполнялось условие симметрии порядков $e_{min}=1-e_{max}$. При этом, если для экспоненты выделено поле размером $E$ битов, то максимально возможные порядки по Стандарту, умещающиеся в это поле, будут такими: $e_{min}=-(2^{E-1}-2)$, а $e_{max}=2^{E-1}-1$, соответственно, $\mathrm{bias}=e_{max}$.

Поясним на нашем примере. У нас $E=3$, то есть $e_{min}=-(4-2)=-2$, $e_{max}=4-1=3$ и $\mathrm{bias}=3$. Получается, что мы можем даже расширить наш пример на два порядка, не выходя за пределы отведённых 3-х битов. Это значит, что теперь наша числовая ось выглядит следующим образом:

Итак, мы выделили 1 бит для хранения знака, 3 бита на порядок числа и 2 бита на хранение дробной части мантиссы. Всего 6 битов, разбитых на три поля. Логика Стандарта требует расположить эти поля друг за другом как на рисунке:

Бит номер 5 (синий) отвечает за знак, биты зелёного цвета (2—4) сохраняют смещённую экспоненту, а красное поле (биты 0—1) хранят дробную часть мантиссы. Рассмотрим несколько примеров точно представимых чисел. Напомню, что мы немного расширили наш пример, в котором диапазон экспонент теперь от −2 до 3, так как решили взять под кодирование порядка 3 бита.

Минимальное нормализованное число имеет значение 1,00(2)×2−2=0,25. Оно положительное, поэтому знак равен 0. Смещённая экспонента равна $e_b=-2+3=1$, а дробная часть мантиссы равна 00. Таким образом, число 0,25 будет закодировано как

0 001 00 = 0,25

Максимально возможное число равно 1,11(2)×23=14. Смещённая экспонента будет равна $e_b=3+3=6$, дробная часть мантиссы 11. Таким образом, получаем набор битов:

0 110 11 = 14

Число −1,25=−1,01(2)×20 запишется как

1 011 01 = −1,25

Внимательный читатель заметил, что мы умещаем в 3-х битах порядка 6 различных экспонент, хотя могли бы уместить 8. Естественный вопрос: куда делось ещё два значения, а именно $e_b=000_{(2)}$ и $e_b=111_{(2)}$?

Два крайних значения $e_b=0$ и $e_b=7$ не участвуют в кодировании порядка числа. По Стандарту эти крайние значения имеют иной смысл и отдаются для записи специальных чисел.

Когда значение $e_b=0$, мы должны интерпретировать числа с плавающей запятой как денормализованные, то есть считать старший лидирующий бит (который мы не храним явно) равным 0, а значение экспоненты считать равной $e_{min}$. Например, число −0,10(2)×2−2=−0,125 является денормализованным, поэтому будет закодировано как

1 000 10 = −0,125

Когда $e_b=7$, числа интерпретируются как бесконечность или «не число» (Not a Number, NaN). Об этих особых числах, а также о нуле, который тоже является особым числом, мы и поговорим дальше.

Нули, бесконечности и NaN'ы

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

  • 0 000 00 = +0
  • 1 000 00 = −0

При этом по Стандарту следует считать справедливым равенство +0=−0.

Положительные и отрицательные нули имеют довольно важное значение. Вспомните математический анализ. Если интерпретировать 0 как предел некоторой последовательности, то с помощью знака нуля мы отражаем то, с какой стороны мы подошли к этому пределу. Таким образом, отрицательный ноль можно интерпретировать как то, что в процессе расчётов мы получили некое очень маленькое отрицательное число, и оно было округлено до нуля. Ещё один смысл знакового нуля мы увидим, познакомившись с бесконечностью.

Вопреки всеобщему мнению, арифметика с плавающей запятой является достаточно сложным и неочевидным в использовании инструментом, поэтому требует от программиста достаточно большого опыта, чтобы полученный в программе результат действительно получился близким к правильному. Заставлять программиста заботиться ещё и о возможном переполнении было бы неправильно, особенно если учесть тот факт, что далеко не всегда это удаётся относительно просто сделать (или не забыть сделать) при работе с целыми числами. Вот одна из причин, по которой Стандарт предусматривает специальное число, называемое «бесконечностью».

Бесконечность (положительная или отрицательная) кодируется следующим образом. Поле экспоненты состоит целиком из единиц ($e_b=7$), а поле мантиссы — из нулей. Бит знака по-прежнему отвечает за знак.

  • 0 111 00 = +∞
  • 1 111 00 = −∞

Бесконечность обладает рядом интуитивно понятных свойств. Например +∞±a=+∞ или −∞<a, если a — конечное число. Связь бесконечности и нуля также подчиняется простым правилам: a/+0=+∞, a/−0=−∞, a/+∞=+0, a/−∞=−0.

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

Что будет, если попытаться рассчитать, скажем +∞/+∞, или −0/−0? В таких случаях предусмотрен ещё ряд специальных значений, называемых «Не Число», или просто NaN (Not a Number). NaN показывает пользователю, что в результате расчётов была выполнена недопустимая операция (скажем, $\sqrt{-1}$ или +∞/+∞), или что один из операндов был не числом (скажем NaN+1,0). Значение NaN кодируется следующим образом: поле экспоненты состоит из единичек ($e_b=7$), а поле мантиссы содержит хотя бы один единичный бит (то есть, не равно нулю). Поле знака не имеет значения.

  • 0 111 10 = NaN
  • 1 111 01 = NaN
  • 1 111 11 = NaN

Важным условием, с помощью которого можно «отловить» NaN, является то, что если x=NaN, то x≠x. Более точно, оператор должен возвращать true, когда получает в качестве любого из своих операндов NaN. Остальные логические операции должны возвращать false. То есть, x≤x = false, что выглядит несколько странно. Подобное правило порождает одну неожиданность, связанную с отрицанием. Мы не можем заменить одну операцию сравнения на противоположную с последующим отрицанием, то есть a≤b — это не то же самое, что NOT(a>b), потому как в случае NaN первое сравнение даст false, а второе — true;. Сравнивая числа с плавающей запятой, имейте в виду эту их особенность, касающуюся NaN.

Стандарт не оговаривает то, какой смысл следует приписывать битам мантиссы для NaN, поэтому производители вычислительных устройств вправе по своему усмотрению придавать значения битам NaN. В мировой практике закпреилось негласное правило, при котором старший бит поля мантиссы полагается равным 1 для так называемого тихого NaN (quite NaN, или qNaN) и 0 для так называемого сигнализирущего NaN (signaling NaN, или sNaN). Разница между ними в том, что qNaN обрабатывается в штатном режиме, не создавая исключений, а sNaN порождает некую аварийную ситуацию (например, в виде исключения).

Разделение NaN на sNaN и qNaN удобно тем, что в ряде случаев NaN следует считать критической ошибкой, о которой некий механизм должен сигнализировать, а в каких-то случаях появление NaN укладывается в концепцию разработчика программы.

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

Можно предложить и другие примеры использования NaN для различных целей, однако каких-то устоявшихся традиций в этой области нет. Более того, если вы — рядовой профессиональный программист, то почти в 100% случаях встреча с NaN будет означать для вас то, что где-то в программе была выполнена недопустимая операция из разряда приведённых выше, причём каких-то дополнительные знания о NaN вам никогда не понадобятся.

Сравнение и порядок чисел

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

Для начала предположим, что знаковый бит s равен нулю, то есть будем работать с неотрицательными числами. Давайте интерпретировать биты числа с плавающей запятой как биты целого числа. Например, число 0 001 00 = 0,25 можно рассматривать как целое число 4, а 0 010 11 = 0,875 — это 11. Важное правило, которому подчиняются числа с плавающей запятой, состоит в том, что их порядок на числовой оси совпадает с порядком соответствующих им целых чисел.

Иными словами, если мы возьмём два неотрицательных числа с плавающей запятой a и b, и соответствующие их представлению целые числа A и B, то оказывается, что a<b тогда и только тогда, когда A<B. Это правило сохраняется даже тогда, когда b=+∞. Более того, если числу с плавающей запятой a соответствует целое число A, то следующему за a точно представимому числу (или бесконечности) соответствует A+1. Например, следом за 0 010 11 = 0,875 идёт 0 011 00 = 1.

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

В случае, когда оно из чисел — NaN, правило работать не будет, потому что эти «не числа» вообще не подчиняются какому-либо порядку, и рассматриваются отдельно. Как уже было сказано, единственный оператор сравнения, который возвращает true с участием NaN, — это .