Разбираемся в числах с плавающей точкой (часть 0)

в 10:10, , рубрики: c++, floating point, math, математика

Здравствуйте. Я давно увлекаюсь темой регистров с плавающей точкой. Меня всегда волновало то, как происходит вывод на экран и т.д. Помню, давным-давно в универе реализовывал свой класс чисел с плавающей точкой, состоящих из 512 бит. Единственное, что я не мог никак реализовать — это вывод на экран.

Как только у меня появилось свободное время, я взялся за старое. Завел себе тетрадку и пошло-поехало. Хотелось додуматься до всего самому, лишь иногда заглядывая в стандарт IEEE 754.
И вот что из всего этого вышло. Интересующихся прошу под кат.

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

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

В сегодняшней статье для примера я буду использовать 32-битные регистры. Числа с двоичной точностью (64-битные) работают абсолютно по той же логике.

Сначала поговорим о том, как хранятся числа с плавающей точкой. Старший 31 бит у нас знаковый. Единичка значит, что число отрицательное, а ноль, соответственно наоборот. Далее идут 8 бит экспоненты. Эти 8 битов представляют из себя обычное беззнаковое число. И в самом конце идут 23 бита мантиссы. Для удобства будем обозначать знак как S, экспоненту как E, а мантиссу, как ни странно, M.

Получаем общую формулу $(-1)^s times M times 2 ^{E-127}$

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

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

Давайте попробуем показать это на наглядном примере:

Представим число 3.625 в двоичном виде. Поначалу разобьем это число на степени двойки. $3.625=2 + 1 + 0.5 + 0.125=1 times 2 ^ 1 + 1 times 2 ^ 0 + 1 times 2 ^ {-1} + 0 times 2 ^ {-2} + 1 times 2 ^ {-3}$

Степень старшей двойки равна единице. E – 127 = 1. E = 128.

0 1000000 11010000000000000000000

Вот и все наше число.

Попробуем также и в обратную сторону. Пусть у нас есть 32 бита, произвольные 32 бита.

0 10000100 (1)11011100101000000000000

В скобочках указан тот самый неявный старший бит.

Сначала вычислим экспоненту. E = 132. Соответственно степень старшей двойки будет равна 5. Итого имеем следующее число:
$2 ^ 5 + 2 ^ 4 + 2 ^ 3 + 2 ^ 1 + 2 ^ 0 + 2 ^ {-1} + 2 ^ {-4} + 2 ^ {-6}=$
$=32 + 16 + 8 + 2 + 1 + 0.5 + 0.0625 + 0.015625=59.578125 $

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

Для удобной конвертации я накидал небольшую программу на языке C.

#include <stdio.h>

union IntFloat {
    unsigned int integerValue;
    float floatValue;
};

void printBits(unsigned int x) {
    int i;
    for (i = 31; i >= 0; i--) {
        if ((x & ((unsigned int)1 << i)) != 0) {
            printf("1");
        }
        else {
            printf("0");
        }
        if (i == 31) {
            printf(" ");
        }
        if (i == 23) {
            printf(" ");
        }
    }
    printf("n");
}

int main() {
    union IntFloat b0;
    b0.floatValue = 59.578125;

    printBits(b0.integerValue);

    b0.integerValue = 0b01000010011011100101000000000000;

    printf("%fn", b0.floatValue);

    return 0;

}

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

Можно выразиться иначе. Два соседних числа с плавающей точкой будут отличаться на 2 ^ (E — 127 — 23). То есть на разницу, равную значению младшего бита.

В качестве доказательства можете поменять main в коде и скомпилировать еще раз.

union IntFloat b0, b1, b2;

    b0.floatValue = 59.578125F;
    b1.integerValue = b0.integerValue + 1;
    b2.floatValue = b1.floatValue - b0.floatValue;

    printBits(b0.integerValue);
    printBits(b1.integerValue);
    printBits(b2.integerValue);


    printf("%fn", b0.floatValue);
    printf("%fn", b1.floatValue);
    printf("%fn", b2.floatValue);

    short exp1 = 0b10000100;
    short exp2 =0b01101101;

    /* Крайний случай, когда вся мантиса состоит из единиц */
    b0.integerValue = 0b01000010011111111111111111111111;
    b1.integerValue = b0.integerValue + 1;
    b2.floatValue = b1.floatValue - b0.floatValue;

    printBits(b0.integerValue);
    printBits(b1.integerValue);
    printBits(b2.integerValue);

    printf("%fn", b0.floatValue);
    printf("%fn", b1.floatValue);
    printf("%fn", b2.floatValue);

    /* Значения экспонент */
    printf("%d %dn", exp1, exp2);

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

P.S.: Я понимаю, что я не затронул тему денормализованных чисел и т.д. Просто не хотелось очень сильно грузить статью, да и эту информацию можно легко найти в стандарте IEEE 754 практически в самом начале.

Автор: Sultansoy

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js