Перейти к содержимому
Topos Uranos

Краткое содержание

Числа с дробной частью в программе не являются действительными числами математики: это конечное подмножество рациональных чисел, знаменатель которых есть степень двойки, и всякое значение, не принадлежащее этому подмножеству, заменяется ближайшим из тех, что ему принадлежат. В этом уроке изучается, как число с плавающей точкой хранится в 64 битах (знак, порядок и мантисса), почему у 0.10.1 нет точного представления и какое значение занимает его место, сколько цифр надёжны в float и в double, как накапливаются погрешности округления, почему два действительных числа не сравнивают с помощью == и как их сравнивают с обоснованным допуском, а также что такое бесконечность и не-число, которые формат резервирует для результатов, не помещающихся в него или не существующих.

Цели обучения

  1. Объяснять, почему у 0.10.1 нет точного двоичного представления и какое значение хранится вместо него.
  2. Предсказывать влияние округления при многократном сложении и при сложении чисел очень разных порядков величины.
  3. Сравнивать действительные числа с подходящим допуском вместо ==, обосновывая выбор допуска.
  4. Выбирать между float и double в зависимости от требуемой точности и распознавать бесконечность и не-число.

Экспоненциальная запись по основанию 2

Урок Целые числа и их представление показал, что целое число из nn бит представляет ровно 2n2^n последовательных значений. Однако числа с дробной частью ставят иную трудность: между любыми двумя действительными числами лежит бесконечно много других, и никакое конечное число битов не может различить их все. Решение, которое принимают все современные компьютеры, то же, каким пользуется наука, чтобы записывать очень большие или очень малые величины: экспоненциальная запись. В десятичной системе среднее расстояние от Земли до Солнца записывается как 1.496×10111.496 \times 10^{11} метров; у числа есть знак, несколько значащих цифр (мантисса, 1.4961.496) и степень основания (порядок, 1111), которая указывает, где стоит точка, отделяющая дробную часть. Если это так, то фиксированным числом цифр охватываются величины самых разных порядков, ценой того, что от каждой хранятся лишь первые цифры.

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

x=(−1)s×1.m1m2m3…m52 (2)×2ex = (-1)^{s} \times 1.m_1 m_2 m_3 \ldots m_{52} \, {}_{(2)} \times 2^{e}

где ss есть знак (0 для положительного, 1 для отрицательного), 1.m1m2…1.m_1 m_2 \ldots есть мантисса, записанная в двоичной системе, первая цифра которой всегда равна 1, а ee есть порядок. То, что первая цифра всегда равна 1, имеет практическое следствие: её не нужно хранить, и сэкономленный бит идёт на точность. Название плавающая точка описывает именно эту форму: точка не занимает фиксированного места среди цифр, а «плавает» в зависимости от порядка.

Тип double в C++ во всех современных компиляторах использует международно стандартизованный двоичный формат двойной точности длиной 64 бита: 1 бит на знак, 11 битов на порядок и 52 бита на мантиссу без начальной цифры. Порядок хранится со смещением: 11-битовое поле содержит e+1023e + 1023, так что оно хранит беззнаковые значения от 1 до 2046 для порядков от −1022-1022 до 10231023 (значения поля 0 и 2047 зарезервированы для нуля, очень малых чисел, бесконечности и не-числа, о которых пойдёт речь в конце урока). Тип float использует формат одинарной точности длиной 32 бита: 1 бит на знак, 8 на порядок и 23 на мантиссу.

Биты double можно увидеть. Программа ниже использует std::bit_cast из заголовочного файла <bit>, которая заново интерпретирует 64 бита числа как беззнаковое целое из 64 битов, не меняя ни одного из них; здесь она используется без дальнейших объяснений, как инструмент для того, чтобы заглянуть в память. Спецификация формата {:064b} записывает это целое в двоичной системе 64 цифрами, как в предыдущем уроке, а {:016X} в шестнадцатеричной 16 цифрами.

Программа на C++
#include <bit>#include <cstdint>#include <print>​int main(){    const double tenth{0.1};    // 64 бита числа, прочитанные как беззнаковое целое.    const std::uint64_t bits{std::bit_cast<std::uint64_t>(tenth)};    std::println("{:064b}", bits);    std::println("{:016X}", bits);}
Вывод
00111111101110011001100110011001100110011001100110011001100110103FB999999999999A
Пример64 бита числа 0.1

Рисунок группирует 64 бита, которые вывела программа, и читает их поле за полем.

Доказательство

  1. 1+11+52=641 + 11 + 52 = 64

    64 бита в шестнадцатеричной записи: 3FB999999999999A. Они читаются слева направо: 1 бит знака, 11 битов порядка и 52 бита мантиссы.

  2. s=0s = 0

    Знак равен 0: число положительное.

  3. e=1019−1023=−4e = 1019 - 1023 = -4

    Поле порядка, 0111111101101111111011 в двоичной системе, равно 1019; из него вычитается смещение 1023, и порядок равен −4-4.

  4. m≈1.6000000000000000888m \approx 1.6000000000000000888

    Мантисса вместе с начальной единицей, которая не хранится, равна 1.1001100110011…10101.1001100110011\ldots1010 в двоичной системе: группа 10011001 повторяется, а последняя группа, 10101010, округлена вверх. Её значение приблизительно равно 1.60000000000000008881.6000000000000000888.

  5. x=0.1000000000000000055511…≠0.1x = 0.1000000000000000055511\ldots \neq 0.1

    Хранимое число равно +1.6000000000000000888×2−4+1.6000000000000000888 \times 2^{-4}, то есть 0.1000000000000000055511…0.1000000000000000055511\ldots: очень близко к 0.10.1, но не 0.10.1.