Арифметика в ЭВМ: числа с фиксированной и плавающей точкой

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

1. Представление чисел в ЭВМ: прямой, обратный и дополнительный коды, формат с фиксированной точкой

Представление чисел в ЭВМ: прямой, обратный и дополнительный коды, формат с фиксированной точкой

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

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

Бит, байт и проблема знака

В основе хранения данных лежит бит — ячейка, принимающая значение 0 или 1. Группы битов образуют байты (8 бит) и машинные слова (16, 32, 64 бита).

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

Где индекс указывает на двоичную систему счисления.

Для записи отрицательных чисел инженеры выделили самый старший (левый) бит под знак числа:

* 0 в старшем бите — число положительное. * 1 в старшем бите — число отрицательное.

Этот принцип используется во всех методах кодирования целых чисел.

Прямой код (Sign-Magnitude)

Это наиболее интуитивный способ. Мы берем двоичную запись модуля числа и устанавливаем знаковый бит.

Рассмотрим 8-битные числа:

* в прямом коде: 00000101 * в прямом коде: 10000101

Формально представление описывается так:

Где — представление числа в прямом коде, а — абсолютное значение (модуль) числа .

Недостатки прямого кода

Несмотря на простоту, прямой код имеет два критических недостатка:

  • Два нуля. Существует «положительный ноль» (00000000) и «отрицательный ноль» (10000000). Это усложняет схемотехнику процессора, так как требуется дополнительная проверка при сравнении чисел.
  • Сложная арифметика. Сложение чисел с разными знаками нельзя выполнить простым суммированием битов. Процессору нужно сравнивать модули, определять знак результата и выполнять вычитание.
  • Обратный код (Ones' Complement)

    Обратный код был попыткой упростить арифметику. Правило формирования:

    * Положительные числа: совпадают с прямым кодом. * Отрицательные числа: получаются путем инверсии всех битов положительного числа (0 меняется на 1, 1 на 0).

    Пример для числа :

  • Берем : 00000101
  • Инвертируем все биты: 11111010
  • Результат 11111010 — это в обратном коде.

    Проблемы обратного кода

    Хотя вычитание стало проще (его можно свести к сложению), проблема двух нулей осталась:

    * : 00000000 * : 11111111 (инверсия всех нулей)

    Из-за этого недостатка обратный код в современных универсальных процессорах практически не используется.

    Дополнительный код (Two's Complement)

    Это стандарт де-факто в современной вычислительной технике. Дополнительный код решает проблему двух нулей и позволяет выполнять вычитание через сложение без дополнительных корректировок.

    Алгоритм получения

    Для положительных чисел код совпадает с прямым. Для отрицательных:

  • Записать модуль числа в двоичном виде.
  • Инвертировать все биты (получить обратный код).
  • Прибавить 1 к младшему разряду.
  • Пример для числа :

  • : 00000101
  • Инверсия: 11111010
  • Прибавляем 1: 11111010 + 1 = 11111011
  • Результат: 11111011 — это в дополнительном коде.

    Математически это выражается формулой для -битного числа:

    Где — представление отрицательного числа в дополнительном коде, — разрядность (количество бит), — модуль числа.

    Преимущества дополнительного кода

  • Единственный ноль. Ноль всегда записывается как 00000000. Инверсия нулей (11111111) плюс единица дает 1 00000000, где единичка переполнения отбрасывается, оставляя чистые нули.
  • Универсальная арифметика. Процессору не нужно знать, вычитает он или складывает. Он просто складывает биты. Например, :
  • Результат 11111101 — это действительно в дополнительном коде (инверсия даст 00000010, плюс 1 = 00000011, то есть 3).

    Числа с фиксированной точкой (Fixed Point)

    До сих пор мы говорили о целых числах. Но как представить дробь, например, или ? Существует два подхода: плавающая точка (Floating Point, стандарт IEEE 754) и фиксированная точка.

    Фиксированная точка — это способ представления дробных чисел, при котором позиция разделителя целой и дробной части жестко зафиксирована.

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

    Формат Q (Q-notation)

    Часто используется запись вида Qn.m, где: * — количество бит для целой части (иногда включая знак). * — количество бит для дробной части.

    Рассмотрим 8-битное число в формате Q4.4 (4 бита целых, 4 бита дробных).

    Пусть в памяти записано число: 00101100.

    Мы мысленно ставим точку посередине: 0010.1100.

    Расчет значения

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

    Где — итоговое значение, — количество дробных бит, — количество целых бит, — значение бита (0 или 1) в позиции , — вес разряда.

    Разберем пример 0010.1100:

    * Целая часть 0010: * Дробная часть 1100:

    Посчитаем дробную часть:

    Итоговое число: .

    Альтернативный взгляд: Масштабирование

    Можно смотреть на это проще: мы храним целое число , а реальное значение получаем делением на (где — количество дробных бит).

    В нашем примере 00101100 как простое целое равно . Поскольку у нас 4 дробных бита, делим на :

    Где — реальное число, — целочисленное представление в памяти, — масштабный коэффициент.

    Плюсы и минусы фиксированной точки

    Преимущества: * Скорость: Операции сложения и вычитания выполняются так же быстро, как с обычными целыми числами. Не нужен сопроцессор плавающей точки (FPU). * Точность: Абсолютная погрешность постоянна по всему диапазону чисел.

    Недостатки: * Ограниченный диапазон: Мы не можем записывать очень большие и очень маленькие числа одновременно (в отличие от плавающей точки). * Сложность умножения: При перемножении двух чисел формата Q4.4 результат будет иметь формат Q8.8, и его нужно приводить обратно к Q4.4, теряя точность.

    Фиксированная точка — основной инструмент в микроконтроллерах, финансовом ПО (где важна точность до копейки и недопустимы ошибки округления плавающей точки) и графике реального времени.

    2. Алгоритмы арифметических операций над числами с фиксированной точкой и обработка переполнения

    Алгоритмы арифметических операций над числами с фиксированной точкой и обработка переполнения

    В предыдущей статье мы разобрали, как числа хранятся в памяти компьютера, и выяснили, что дополнительный код (Two's Complement) является стандартом для представления знаковых целых чисел. Теперь пришло время заставить эти числа работать.

    Как процессор складывает числа? Почему вычитание — это на самом деле сложение? И что происходит, когда результат вычисления становится слишком большим, чтобы поместиться в отведенную память? В этой статье мы разберем алгоритмы арифметики с фиксированной точкой и научимся предсказывать и обрабатывать переполнения.

    Сложение и вычитание в дополнительном коде

    Главное преимущество дополнительного кода заключается в том, что он унифицирует операции сложения и вычитания. Для процессора не существует операции «вычесть из ». Вместо этого он выполняет операцию «прибавить к отрицательное ».

    Где — уменьшаемое, — вычитаемое, а — представление числа в дополнительном коде.

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

    Пример сложения чисел с разными знаками

    Рассмотрим сложение чисел и в 8-битной системе.

  • Число в двоичном виде: .
  • Число в двоичном виде: .
  • Число в дополнительном коде: инвертируем биты () и прибавляем 1 .
  • Теперь выполним сложение столбиком:

    Здесь мы видим результат . Обратите внимание, что результат получился 9-битным. Так как мы работаем в 8-битной арифметике, старший девятый бит (единица слева) просто отбрасывается (теряется).

    Оставшиеся 8 бит: . В десятичной системе это число . Проверка: . Всё верно.

    Этот механизм работает безупречно, пока результат помещается в доступное количество бит. Но что, если нет?

    Проблема переполнения (Overflow)

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

    Для -битного знакового числа диапазон значений составляет:

    Где — количество бит, — модуль минимального числа, а — максимальное положительное число.

    Для 8 бит этот диапазон: от до .

    Как обнаружить переполнение?

    В знаковый арифметике переполнение может возникнуть только при сложении чисел с одинаковыми знаками.

    * Если мы складываем два положительных числа и получаем отрицательное — произошло переполнение. * Если мы складываем два отрицательных числа и получаем положительное — произошло переполнение. * При сложении чисел с разными знаками переполнение невозможно (результат всегда будет ближе к нулю, чем слагаемые по модулю).

    !Визуализация переполнения как движения по кругу: переход границы между максимальным положительным и минимальным отрицательным числом.

    Пример переполнения

    Попробуем сложить и в 8-битной системе.

    * *

    Результат: . В знаковом представлении старший бит означает отрицательное число. Это код числа .

    Мы получили: . Это классический пример знакового переполнения.

    В процессорах для фиксации этого события существует специальный флаг OF (Overflow Flag). Если после операции знак результата не совпадает со знаком слагаемых (при условии, что слагаемые имели одинаковый знак), флаг OF устанавливается в 1.

    Умножение чисел с фиксированной точкой

    Умножение сложнее сложения. Если при сложении двух -битных чисел результат (обычно) помещается в бит (исключая переполнение), то при умножении двух -битных чисел результат может занимать до бит.

    Например, умножая два 8-битных числа, мы должны быть готовы сохранить 16-битный результат.

    Алгоритм умножения

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

    Если мы умножаем числа с фиксированной точкой, мы фактически умножаем их как целые числа, а затем должны правильно интерпретировать положение точки.

    Пусть у нас есть два числа в формате с фиксированной точкой. Обозначим их как и . Пусть формат представления имеет дробных бит (например, Q16.16, где ).

    Математически значение числа равно:

    Где — реальное значение числа, — целое число, хранящееся в битах, а — масштабный коэффициент.

    При умножении:

    Где — искомое произведение, — результат целочисленного умножения, а — новый масштабный коэффициент.

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

    Пример умножения с фиксированной точкой

    Представим, что у нас 4-битная система, где 2 бита отведены под дробную часть (формат Q2.2). Масштабный коэффициент .

    Число в десятичной системе в «сыром» виде (). Число в десятичной системе в «сыром» виде ().

    Умножаем «сырые» значения:

    В двоичном виде это .

    Теперь вспоминаем про точку. У нас было 2 дробных бита у первого числа и 2 у второго. В результате у нас теперь 4 дробных бита. Число интерпретируется как (если считать все 4 бита дробными).

    Чтобы вернуть формат Q2.2, нам нужно сдвинуть результат вправо на 2 бита (отбросить лишнюю точность или округлить).

    () сдвигаем вправо на 2 ().

    Проверяем: «сырое» в формате Q2.2 означает . Математически: . Всё верно.

    Деление чисел с фиксированной точкой

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

    Где и — целочисленные представления. Заметьте, что множители сокращаются.

    Если мы просто разделим целые части, мы получим целое число без дробной части. Например, в целочисленной арифметике даст . Нам это не подходит.

    Чтобы сохранить точность, перед делением делимое () нужно сдвинуть влево на бит (умножить на ).

    Где — результат в формате с фиксированной точкой.

    Особенности реализации

  • Потеря точности: При сдвигах вправо (после умножения) мы теряем младшие биты. Это вносит ошибку округления.
  • Переполнение при делении: Сдвиг делимого влево может вызвать переполнение еще до начала операции деления. Для этого часто используют расширенные регистры (например, 64-битный регистр для деления 32-битных чисел).
  • Арифметика с насыщением (Saturation Arithmetic)

    В стандартной арифметике переполнение приводит к «оборачиванию» числа (wrap-around): . Это математически корректно по модулю , но катастрофично для обработки сигналов (звука, графики).

    Представьте, что вы увеличиваете громкость звука. Если значение амплитуды превысит максимум и станет отрицательным, вместо громкого звука вы услышите резкий треск (инверсию волны).

    Для решения этой проблемы используется арифметика с насыщением.

    Правило простое: если результат операции превышает максимум, он остается равным максимуму. Если меньше минимума — остается равным минимуму.

    * Обычная арифметика: * Арифметика с насыщением:

    Этот подход требует дополнительных проверок флага переполнения после каждой операции, но он критически важен в мультимедийных приложениях и DSP (цифровой обработке сигналов).

    Заключение

    Арифметика с фиксированной точкой — это мощный инструмент, который позволяет выполнять дробные вычисления, используя быстрые целочисленные блоки процессора. Главное — помнить о положении воображаемой запятой и внимательно следить за переполнениями.

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

    3. Стандарт IEEE 754: структура, нормализация и диапазоны чисел с плавающей точкой

    Стандарт IEEE 754: структура, нормализация и диапазоны чисел с плавающей точкой

    В предыдущей лекции мы подробно разобрали арифметику с фиксированной точкой. Мы выяснили, что это быстрый и эффективный способ вычислений, но он обладает критическим недостатком: узким динамическим диапазоном. Мы не можем одной и той же «линейкой» измерять и диаметр атома, и расстояние до соседней галактики.

    Чтобы решить эту проблему, инженеры обратились к научной нотации записи чисел. Так появился формат с плавающей точкой (floating point), который сегодня является стандартом де-факто во всех современных вычислительных системах. Этот стандарт известен как IEEE 754.

    От научной нотации к машинному слову

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

    В общем виде это выглядит так:

    Где — записываемое число, — мантисса (значащая часть), — основание системы счисления (в науке обычно 10), а — экспонента (порядок).

    Компьютеры используют двоичную систему, поэтому для них основание . Стандарт IEEE 754 описывает, как именно упаковать знак, мантиссу и экспоненту в ограниченное количество бит (32 или 64), чтобы обеспечить максимальную точность и диапазон.

    Структура числа IEEE 754

    Рассмотрим самый популярный формат: одинарная точность (Single Precision, 32 бита), который в языках программирования часто называется float.

    32 бита распределяются следующим образом:

  • Знак (Sign, S): 1 бит. Определяет, положительное число или отрицательное.
  • Экспонента (Exponent, E): 8 бит. Определяет порядок числа (масштаб).
  • Мантисса (Mantissa, M): 23 бита. Содержит значащие цифры числа.
  • !Структура 32-битного числа с плавающей точкой согласно стандарту IEEE 754.

    Формула для вычисления значения числа выглядит так:

    Где — итоговое значение числа, — значение бита знака (0 или 1), — нормализованная мантисса (дробная часть), — значение экспоненты как целого беззнакового числа, а — смещение экспоненты (константа).

    Давайте разберем каждый элемент этой формулы подробнее.

    1. Знак (Sign)

    Здесь всё просто, как и в числах с фиксированной точкой: * 0 — число положительное. * 1 — число отрицательное.

    2. Нормализация и скрытая единица

    В научной нотации принято записывать число так, чтобы перед запятой стояла ровно одна значащая цифра (не ноль). Например, мы пишем , а не или .

    В двоичной системе единственная возможная ненулевая цифра — это 1. Следовательно, любое нормализованное двоичное число выглядит как:

    Где — целая часть, — дробная часть, а — множитель масштаба.

    Создатели стандарта IEEE 754 пошли на хитрость. Раз целая часть нормализованного числа всегда равна 1, зачем тратить драгоценный бит памяти на её хранение? Мы можем просто подразумевать, что она там есть.

    Поэтому в 23 битах мантиссы хранится только дробная часть (то, что стоит после запятой). При восстановлении числа процессор автоматически добавляет к мантиссе единицу. Это называется скрытая единица (implicit leading bit). Фактически, имея 23 бита памяти, мы получаем точность в 24 бита.

    3. Смещенная экспонента (Biased Exponent)

    Экспонента может быть как положительной (для больших чисел, например ), так и отрицательной (для маленьких чисел, например ).

    Чтобы не возиться с дополнительным кодом для экспоненты и упростить операции сравнения чисел, стандарт использует смещение (Bias). Экспонента хранится как беззнаковое целое число, но при вычислении реального значения из него вычитается константа.

    Для типа float (8 бит экспоненты) смещение .

    * Если в битах экспоненты записано (), реальная степень равна . * Если записано (), реальная степень равна . * Если записано (), реальная степень равна .

    Диапазон хранимых значений экспоненты: от 0 до 255. Однако значения 0 и 255 зарезервированы для специальных случаев (о них ниже), поэтому реальный диапазон степеней двойки: от до .

    Пример: Перевод числа в формат IEEE 754

    Попробуем вручную перевести число -13.625 в формат float.

    Шаг 1. Определяем знак. Число отрицательное, значит бит знака .

    Шаг 2. Переводим модуль числа в двоичный вид. Целая часть: . Дробная часть: (так как ). Итого: .

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

    Где — мантисса с целой частью, а — реальная степень.

    Шаг 4. Вычисляем смещенную экспоненту. Реальная степень . Хранимая экспонента . Переводим 130 в двоичный вид: .

    Шаг 5. Формируем мантиссу. Берем дробную часть от нормализованного числа (). Отбрасываем ведущую единицу (она скрытая). Дополняем нулями справа до 23 бит. Мантисса: .

    Результат:

    В шестнадцатеричном виде: 0xC15A0000.

    Специальные значения

    В стандарте IEEE 754 есть комбинации битов, которые интерпретируются особым образом. Они кодируются с помощью зарезервированных значений экспоненты (все нули или все единицы).

    | Экспонента | Мантисса | Значение | | :--- | :--- | :--- | | 0 (00...0) | 0 (00...0) | Нуль (Zero). Причем существует и . | | 0 (00...0) | Не 0 | Денормализованные числа. Очень маленькие числа, близкие к нулю, у которых нет скрытой единицы. | | 255 (11...1) | 0 (00...0) | Бесконечность (). Возникает при переполнении или делении на ноль. Бывает и . | | 255 (11...1) | Не 0 | NaN (Not a Number). Результат некорректных операций, например или . |

    Диапазоны и точность

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

    Одинарная точность (float, 32 бита)

    * Диапазон: примерно от до . * Точность: около 7 десятичных знаков. Это значит, что число может быть сохранено только приблизительно (например, как ).

    Двойная точность (double, 64 бита)

    Использует 11 бит под экспоненту и 52 бита под мантиссу. Смещение экспоненты . * Диапазон: примерно от до . * Точность: около 15-16 десятичных знаков.

    Заключение

    Стандарт IEEE 754 — это компромисс между точностью и диапазоном. Благодаря скрытой единице и смещенной экспоненте, мы можем эффективно хранить и обрабатывать числа колоссальных величин.

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

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

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

    В предыдущей статье мы познакомились со стандартом IEEE 754 и узнали, как компьютеры упаковывают огромный диапазон значений в 32 или 64 бита. Мы выяснили, что число с плавающей точкой состоит из трех компонентов: знака, экспоненты (порядка) и мантиссы.

    Однако хранение данных — это лишь полдела. Главная задача процессора — вычислять. И если умножение чисел с плавающей точкой выполняется относительно просто (нужно перемножить мантиссы и сложить экспоненты), то сложение и вычитание — это настоящая алгоритмическая полоса препятствий.

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

    Проблема разных масштабов

    Представьте, что вам нужно сложить 1 километр и 500 метров. Вы не можете просто сложить и получить . Сначала вам нужно привести величины к одной единице измерения: либо , либо .

    То же самое происходит с числами в научной нотации. Попробуем сложить два числа:

    Где — первое слагаемое, представленное как умноженное на в степени .

    Где — второе слагаемое, представленное как умноженное на в степени .

    Мы не можем сложить мантиссы и напрямую, так как они имеют разный «вес». Число в 100 раз «тяжелее» числа . Чтобы выполнить сложение, мы должны привести число с меньшим порядком к порядку большего числа.

    Теперь порядки равны (), и мы можем сложить мантиссы:

    Этот процесс называется выравниванием порядков. В компьютере он происходит в двоичной системе, но принцип остается тем же.

    Алгоритм сложения/вычитания IEEE 754

    Процесс выполнения арифметической операции в арифметико-логическом устройстве (АЛУ) процессора можно разделить на несколько ключевых этапов.

    !Схема этапов сложения чисел с плавающей точкой

    Этап 1: Распаковка и восстановление скрытой единицы

    Числа хранятся в памяти в упакованном виде. Первым делом процессор разделяет их на части: * Знак () * Экспонента () * Мантисса ()

    Важный момент: в формате IEEE 754 целая часть нормализованного числа (единица) не хранится явно. На этом этапе процессор «вспоминает» о ней и добавляет к мантиссе слева. Теперь мантисса имеет вид

    Этап 2: Выравнивание порядков (Alignment)

    Пусть у нас есть два числа с экспонентами и . Процессор сравнивает их и находит разность:

    Где — разница между экспонентами.

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

    Почему сдвигаем именно меньшее число? Если мы будем сдвигать большее число влево (уменьшая его экспоненту), мы можем получить очень большое целое число, которое переполнит регистр. Сдвиг меньшего числа вправо безопаснее: мы просто теряем младшие биты (точность), но сохраняем порядок величины.

    Этап 3: Сложение или вычитание мантисс

    Теперь, когда экспоненты равны, мы работаем только с мантиссами. Операция зависит от знаков чисел: * Если знаки одинаковы ( или ), мантиссы складываются. * Если знаки разные (), из большей мантиссы вычитается меньшая, а результат получает знак большего числа.

    На этом этапе мы получаем промежуточный результат мантиссы ().

    Этап 4: Нормализация результата

    После сложения мантисс результат может перестать быть нормализованным. Возможны два случая нарушения стандарта:

    Случай А: Переполнение мантиссы (Overflow) При сложении двух чисел вида результат может стать больше или равен 2 (например, , что в двоичном виде ). Стандарт требует, чтобы перед запятой стояла только одна единица. * Действие: Сдвигаем мантиссу результата вправо на 1 бит. * Коррекция экспоненты: Увеличиваем экспоненту результата на 1.

    Случай Б: Потеря значимости (Underflow) При вычитании близких чисел результат может стать меньше 1 (например, , что в двоичном виде ). Ведущая единица исчезла, появились ведущие нули. * Действие: Сдвигаем мантиссу результата влево, пока первая единица не окажется перед запятой. * Коррекция экспоненты: Уменьшаем экспоненту на количество сделанных сдвигов.

    Этап 5: Округление и упаковка

    Мантисса результата может не поместиться в отведенные 23 бита (для float). Лишние биты необходимо отбросить, используя один из режимов округления (обычно — к ближайшему четному). После этого отбрасывается ведущая единица (она снова становится скрытой), и число упаковывается обратно в формат IEEE 754.

    Практический пример

    Попробуем сложить два числа в 4-битной упрощенной модели (для наглядности), где мантисса имеет 3 бита после запятой.

    Сложим и .

    1. Представление чисел:

    * Число А: Мантисса , Экспонента * Число Б: Мантисса , Экспонента

    2. Выравнивание: Разница экспонент: . Число Б имеет меньшую экспоненту. Сдвигаем его мантиссу вправо на 3 позиции. Было: Сдвиг 1: (эксп ) Сдвиг 2: (эксп ) Сдвиг 3: (эксп )

    Теперь оба числа имеют экспоненту . Мантисса Б стала .

    3. Сложение:

    Результат: при экспоненте .

    4. Нормализация: Число уже нормализовано (одна единица перед запятой). Действий не требуется.

    5. Результат: . Ответ верный.

    Потеря точности и «съедание» малых чисел

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

    Представьте, что вы складываете и . В формате float (одинарная точность) мантисса вмещает около 7 десятичных цифр. Чтобы выровнять порядки, число нужно сдвинуть вправо на 20 позиций. Но мантисса имеет всего 23 бита! Единица просто «выпадет» за пределы разрядной сетки и превратится в ноль.

    Для компьютера это равенство истинно. Маленькое число было полностью поглощено большим. Это явление называется поглощением (absorption).

    Катастрофическая отмена (Catastrophic Cancellation)

    Еще более опасная ситуация возникает при вычитании двух очень близких чисел.

    Допустим, мы вычитаем и . Оба числа имеют 7 значащих цифр. Результат: .

    В нормализованном виде это . Но обратите внимание: у нас осталась всего одна значащая цифра точности! Остальные цифры мантиссы будут заполнены нулями, которые не несут полезной информации. Мы потеряли 6 порядков точности за одну операцию. Если этот результат будет использоваться в дальнейших вычислениях (например, делении), ошибка будет нарастать лавинообразно.

    Заключение

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

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

    В следующей статье мы рассмотрим операции умножения и деления, которые, как ни странно, структурно проще сложения, но имеют свои подводные камни.

    5. Умножение и деление вещественных чисел, анализ погрешностей и особые ситуации

    Умножение и деление вещественных чисел, анализ погрешностей и особые ситуации

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

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

    Почему в Python или Java не равно ? Что такое «машинный ноль»? И что произойдет, если разделить бесконечность на бесконечность? Давайте разбираться.

    Алгоритм умножения чисел с плавающей точкой

    Вспомним формулу представления числа в стандарте IEEE 754:

    Где — значение числа, — знак, — дробная часть мантиссы, — хранимая экспонента, — смещение (127 для float).

    При умножении двух чисел и мы фактически выполняем действия над степенями:

    Где — полные мантиссы (со скрытой единицей), а — реальные порядки чисел.

    Процесс умножения в процессоре разбивается на четыре независимых этапа.

    1. Вычисление знака

    Это самая простая часть. При умножении (и делении) знак результата отрицателен, если знаки операндов различаются. В двоичной логике это операция XOR (исключающее ИЛИ) над знаковыми битами.

    * * *

    2. Сложение экспонент

    Здесь кроется нюанс. В памяти мы храним не реальные экспоненты, а смещенные (). Если мы просто сложим два хранимых значения, то смещение удвоится:

    Где — реальные порядки, а — константа смещения.

    Нам же нужно, чтобы в результате получилось число с одним смещением (). Поэтому из суммы хранимых экспонент необходимо вычесть одно значение :

    Где — новая хранимая экспонента результата, и — экспоненты исходных чисел.

    Для формата float (где ) это означает, что процессор складывает два 8-битных числа и вычитает 127.

    3. Умножение мантисс

    Мантиссы перемножаются как обычные целые числа с фиксированной точкой. Поскольку каждая мантисса нормализована (находится в диапазоне ), их произведение будет находиться в диапазоне:

    Где — результат перемножения мантисс.

    Это означает, что результат может быть либо вида , либо (то есть больше или равен 2).

    4. Нормализация и округление

    Если произведение мантисс получилось больше или равно 2 (например, , что в двоичном виде ), результат нужно нормализовать:

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

    ![Блок-схема процесса умножения чисел с плавающей точкой внутри процессора

    Алгоритм деления

    Деление симметрично умножению, но операции инвертируются.

    Где — частное мантисс, а — разность порядков.

  • Знак: Также вычисляется через XOR.
  • Экспонента: Мы вычитаем экспоненты. При этом смещения уничтожаются (). Чтобы вернуть формат к стандарту, нужно прибавить обратно:
  • Где — экспонента результата.
  • Мантисса: Делим мантиссу делимого на мантиссу делителя. Результат деления двух чисел из диапазона будет лежать в диапазоне .
  • Нормализация: Если результат меньше 1 (например, ), сдвигаем мантиссу влево и уменьшаем экспоненту.
  • Анализ погрешностей: почему 0.1 + 0.2 != 0.3

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

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

    Любая дробь, знаменатель которой не является степенью двойки, в двоичной системе будет бесконечной периодической дробью. Число (одна десятая) — это . Знаменатель . Наличие множителя 5 гарантирует, что в двоичном виде это будет бесконечная дробь:

    Где последовательность повторяется бесконечно.

    Компьютер имеет всего 23 бита для хранения мантиссы. Он вынужден обрезать эту последовательность и округлить её. В итоге в памяти хранится не ровно , а число, очень близкое к нему: примерно

    Когда мы складываем «почти 0.1» и «почти 0.2», ошибки суммируются, и результат оказывается чуть больше, чем «почти 0.3». Проверка на равенство if (a + b == 0.3) выдаст False.

    Машинный эпсилон

    Как же тогда сравнивать числа? Для этого вводится понятие машинного эпсилона (). Это минимальное число, которое при прибавлении к 1.0 дает результат, отличный от 1.0.

    Для одинарной точности (float) . Для двойной точности (double) .

    Сравнение чисел должно происходить так: «равны ли числа с точностью до эпсилон?»

    Где — модуль разности чисел, а — допустимая погрешность.

    Особые ситуации и исключения

    Стандарт IEEE 754 определяет четкие правила для ситуаций, когда результат не может быть представлен обычным числом.

    1. Деление на ноль

    В школьной математике делить на ноль нельзя. В арифметике с плавающей точкой — можно. Результат зависит от знака:

    * (Плюс бесконечность) * (Минус бесконечность)

    Бесконечности ведут себя логично: , . Но — это неопределенность.

    2. NaN (Not a Number)

    Если операция не имеет математического смысла, процессор возвращает специальное значение NaN (не число). Примеры:

    * * * * (для вещественных чисел)

    Главное свойство NaN — он «токсичен». Любая операция с NaN дает в результате NaN ().

    Важно: NaN не равен ничему, даже самому себе. Выражение NaN == NaN вернет False. Это единственный случай в программировании, когда переменная не равна сама себе. Для проверки используется функция isNaN().

    3. Переполнение (Overflow) и исчезновение (Underflow)

    * Overflow: Если число по модулю становится больше максимального ( для float), оно превращается в . Underflow: Если число становится меньше минимального нормализованного (), оно теряет точность и превращается в денормализованное* число, а затем в ноль.

    Заключение

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

    Главный урок, который нужно вынести: никогда не используйте числа с плавающей точкой для финансовых расчетов, где важна каждая копейка. Для денег существуют специальные типы данных (например, Decimal), работающие по принципам фиксированной точки или десятичной арифметики, о которых мы говорили в начале курса.

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