Что такое плавающая точка в процессоре
Обновлено: 07.07.2024
Важной частью архитектуры микропроцессоров Intel является наличие устройства для обработки числовых данных в формате с плавающей точкой, называемого математическим сопроцессором . Архитектура компьютеров на базе микропроцессоров вначале опиралась исключительно на целочисленную арифметику. С ростом мощи стали появляться устройства для обработки чисел с плавающей точкой. В архитектуре семейства микропроцессоров Intel 8086 устройство для обработки чисел с плавающей точкой появилось в составе компьютера на базе микропроцессора i8086/88 и получило название математический сопроцессор или просто сопроцессор. Выбор такого названия был обусловлен тем, что,
- во-первых, это устройство было предназначено для расширения вычислительных возможностей основного процессора;
- во-вторых, оно было реализовано в виде отдельной микросхемы, то есть его присутствие было необязательным. Микросхема сопроцессора для микропроцессора i8086/88 имела название i8087.
С появлением новых моделей микропроцессоров Intel совершенствовались и сопроцессоры, хотя их программная модель осталась практически неизменной. Как отдельные (а, соответственно, необязательные в конкретной комплектации компьютера) устройства, сопроцессоры сохранялись вплоть до модели микропроцессора i386 и имели название i287 и i387 соответственно. Начиная с модели i486, сопроцессор исполняется в одном корпусе с основным микропроцессором и, таким образом, является неотъемлемой частью компьютера.
Основные возможности математического сопроцессора:
Форма представления чисел с плавающей точкой описана здесь .
Общая форма представления вещественных чисел предполагает возможность размещения в разрядной сетке следующих типов.
Числа простой и двойной точности ( float ( DD ) и double ( DQ ) соответственно) могут быть представлены только в нормированной форме. При этом бит целой части числа является скрытым и подразумевает логическую 1. Остальные 23 (52) разряда хранят двоичную мантиссу числа.
Числа двойной расширенной точности ( long double ( DT )) могут быть представлены как в нормированной, так и в ненормированной форме, поскольку бит целой части числа не является скрытым и может принимать значения как 0, так и 1.
Основным типом данных, которыми оперирует математический сопроцессор, являются 10-байтные данные ( DT ).
Программная модель сопроцессора
Программная модель сопроцессора представляет собой совокупность регистров, каждый из которых имеет свое функциональное назначение.
В программной модели сопроцессора можно выделить три группы регистров:
Все указанные регистры являются программно доступными. Однако к одним из них доступ получить достаточно легко, для этого в системе команд сопроцессора существуют специальные команды. К другим регистрам получить доступ сложнее, так как специальных команд для этого нет, поэтому необходимо выполнить дополнительные действия.
Регистр состояния swr – отражает текущее состояние сопроцессора после выполнения последней команды. В регистре swr содержатся поля, позволяющие определить: какой регистр является текущей вершиной стека сопроцессора, какие исключения возникли после выполнения последней команды, каковы особенности выполнения последней команды (некий аналог регистра флагов основного процессора).
Структурно регистр swr состоит из:
-
6 флагов исключительных ситуаций PE, OE, UE, ZE, DE, IE.
Исключения — это разновидность прерываний, с помощью которых процессор информирует программу о некоторых особенностях ее реального исполнения. Сопроцессор также обладает способностью возбуждения подобных прерываний при возникновении определенных ситуаций (не обязательно ошибочных). Все возможные исключения сведены к 6и типам, каждому из которых соответствует 1 бит в регистре swr . Программисту не обязательно писать обработчик для реакции на ситуацию, приведшую к некоторому исключению. Сопроцессор умеет самостоятельно реагировать на многие из них. Это так называемая обработка исключений по умолчанию. Для того чтобы вызвать обработку определенного типа исключения по умолчанию, необходимо это исключение оставить не маскированным. Такое действие выполняется с помощью установки в 1 соответствующего бита в управляющем регистре сопроцессора cwr . Типы исключений, фиксируемые с помощью регистра swr:
- IE (Invalide operation Error) — недействительный код операция;
- DE (Denormalized operand Error) — ненормированный операнд;
- ZE (divide by Zero Error) — ошибка деления на нуль;
- ОЕ (Overflow Error) — ошибка переполнения. Возникает в случае выхода порядка числа за максимально допустимый диапазон;
- UE (Underflow Error) — ошибка антипереполнения. Возникает, когда результат слишком мал (близок к нулю);
- РЕ (Precision Error) — ошибка точности. Устанавливается, когда сопроцессору приходится округлять результат из-за того, что его точное представление невозможно. Так, сопроцессору никогда не удастся точно разделить 10 на 3.
Регистр управления работой сопроцессора cwr – определяет особенности обработки числовых данных. С помощью полей в регистре cwr можно регулировать точность выполнения численных вычислений, управлять округлением, маскировать исключения.
Он состоит из:
- шести масок исключений PM, UM, OM, ZM, DM, IM ;
- поля управления точностью PC (Precision Control);
- поля управления округлением RC (Rounding Control).
2-битовое поле управления точностью PC предназначено для выбора длины мантиссы. Возможные значения в этом поле означают:
- PC =00 — длина мантиссы 24 бита;
- PC =10 — длина мантиссы 53 бита;
- PC =11 — длина мантиссы 64 бита.
По умолчанию устанавливается значение поля PC =11.
Поле управления округлением RC позволяет управлять процессом округления чисел в процессе работы сопроцессора. Необходимость операции округления может появиться в ситуации, когда после выполнения очередной команды сопроцессора получается не представимый результат, например, периодическая дробь. Установив одно из значений в поле RC , можно выполнить округление в необходимую сторону.
Значения поля RC с соответствующим алгоритмом округления:
- 00 — значение округляется к ближайшему числу, которое можно представить в разрядной сетке регистра сопроцессора;
- 01 — значение округляется в меньшую сторону;
- 10 — значение округляется в большую сторону;
- 11 — производится отбрасывание дробной части числа. Используется для приведения значения к форме, которая может использоваться в операциях целочисленной арифметики.
Бит 12 в регистре cwr физически отсутствует и считывается равным 0.
Регистр тегов twr – представляет собой совокупность двухбитовых полей. Каждое поле соответствует определенному физическому регистру стека и характеризует его текущее состояние. Команды сопроцессора используют этот регистр, например, для того, чтобы определить возможность записи значений в эти регистры. Изменение состояния любого регистра стека отражается на содержимом соответствующего этому регистру 2-битового поля регистра тега. Возможны следующие значения в полях регистра тега:
- 00 — регистр стека сопроцессора занят допустимым ненулевым значением;
- 01 — регистр стека сопроцессора содержит нулевое значение;
- 10 — регистр стека сопроцессора содержит одно из специальных численных значений, за исключением нуля;
- 11 — регистр пуст и в него можно производить запись. Это значение в двухбитовом поле регистра тегов не означает, что все биты соответствующего регистра стека должны быть обязательно нулевыми.
Принцип работы сопроцессора
Принцип работы сопроцессора совместно с центральным процессором
Процессор и сопроцессор имеют свои раздельные системы команд и форматы обрабатываемых данных. Несмотря на то, что сопроцессор архитектурно представляет собой отдельное вычислительное устройство, он не может существовать отдельно от основного процессора. Процессор и сопроцессор, являясь двумя самостоятельными вычислительными устройствами, могут работать параллельно. Но это распараллеливание распространяется только на выполнение команд. Оба процессора подключены к общей системной шине и имеют доступ к одной и той же информации. Инициирует процесс выборки очередной команды всегда основной процессор. После выборки команда попадает одновременно в оба процессора. Любая команда сопроцессора имеет код операции, первые пять бит, которого имеют значение 11011. Когда код операции начинается этими битами, то основной процессор по дальнейшему содержимому кода операции выясняет, требует ли данная команда обращения к памяти. Если это так, то основной процессор формирует физический адрес операнда и обращается к памяти, после чего содержимое ячейки памяти выставляется на шину данных. Если обращение к памяти не требуется, то основной процессор заканчивает работу над данной командой (не делая попытки ее исполнения) и приступает к декодированию следующей команды из текущего входного командного потока. Выбранная команда попадает в сопроцессор одновременно с основным процессором. Сопроцессор, определив по первым пяти битам, что очередная команда принадлежит его системе команд, начинает ее исполнение. Если команда требует операнды из памяти, то сопроцессор обращается к шине данных за чтением содержимого ячейки памяти, которое к этому моменту предоставлено основным процессором.
В определенных случаях необходимо согласовывать работу обоих устройств. К примеру, если во входном потоке сразу за командой сопроцессора следует команда основного процессора, использующая результаты работы предыдущей команды, то сопроцессор не успеет выполнить свою команду за то время, пока основной процессор, пропустив сопроцессорную команду, выполнит свою. При этом что логика работы программы будет нарушена. Возможна и другая ситуация. Если входной поток команд содержит последовательность из нескольких команд сопроцессора, то процессор пропустит их очень быстро, но он должен обеспечить внешний интерфейс для сопроцессора. Эти и другие, более сложные ситуации, приводят к необходимости синхронизации между собой работы двух процессоров. В первых моделях микропроцессоров это делалось путем вставки перед или после каждой команды сопроцессора специальной команды wait или fwait . Работа данной команды заключалась в приостановке работы основного процессора до тех пор, пока сопроцессор не закончит работу над последней командой. В моделях микропроцессора (начиная с i486) подобная синхронизация выполняется автоматически. Но для некоторых команд из группы команд управления сопроцессором оставлена возможность выбора между командами с синхронизацией (ожиданием) и без нее.
Цель статьи - дать читателю представление о работе с числами с плавающей точкой в программировании. Предполагается дать базовый объём информации и предоставить уровень понимания, достаточный для самостоятельного проведения оценок в задачах, возникающих на практике.
Основная причина создания статьи - наблюдаемый недостаток понимания чисел с плавающей точкой среди программистов - как опасностей так и гарантий. В отсутствие понимания гарантий борьба с опасностями ведётся методами, которые часто можно охарактеризовать как "шаманские".
Второй важной причиной стало отсутствие (насколько мне известно) на русском языке статьи по данному вопросу, сравнимой по полноте изложения с классической статьёй Голдберга. Ближайшая известная альтернатива - всё же намного менее подробна.
Статья может быть использована как учебник или как справочник ("или" - не исключающее).
Статья получилась достаточно объёмной, причём материал структурирован в логической последовательности (для облегчения использования статьи в качестве справочника), а не "от простого к сложному". В связи с этим приношу свои извинения новичкам, желающим краткого введения в работу с числами с плавающей точкой.
В качестве языков примеров используются C/C++.
Все отрывки кода в статье, если явно не оговорено обратное, находятся в общем доступе (public domain).
В статье везде используется термин "числа с плавающей точкой", вместо распространённого в русскоязычной математической литературе "числа с плавающей запятой" (а также - соответствующая типография).
Рис. 1. Hydrodynamic number-crunching, автор: Guy L. Steele, Jr.
Вычисления с вещественными числами были одним из основных применений компьютеров с момента их появления. Уже механическая машина Z1, построенная в 1938 г. Конрадом Цузе проводила операции с числами с плавающей точкой в формате, удивительно напоминающем современный.
Т. к. множество вещественных чисел бесконечно, в отличие от памяти компьютеров, то формат представления даёт возможность выразить лишь некоторое подмножество вещественных чисел.
Требования к такому подмножеству включают как требования к точности и диапазону, так и сохранение, по возможности, свойств, интуитивно ожидаемых для вещественных чисел.
На протяжении нескольких десятилетий формат чисел с плавающей точкой был индивидуальным для каждого класса машин. Это снижало совместимость и заметно усложняло написание переносимых математических библиотек. Также в таких условиях сложно было получить гарантии точности вычислений (напр. оценить сверху ошибку). Кроме того идиосинкразии конкретных архитектур делали результаты менее интуитивными, иногда - без достаточно веских на то оснований.
Процесс стандартизации занял 9 лет (1976-1985), окончательный текст был основан на "K-C-S proposal" (авторы: William Kahan, Jerome Coonen, Harold Stone).
Стандарт предоставил единый для всех систем набор форматов и удивительно высокие гарантии (с т. з. разработчиков реализаций - требования) точности.
На данный момент большинство процессоров, вообще поддерживающих вычисления с плавающей точкой, поддерживают именно IEEE 754.
Таким образом, эту часть изначальной задачи стандарта во многом можно считать выполненной.
В 1989 г. Уильям Кэхэн получил премию Тьюринга с формулировкой "for his fundamental contributions to numerical analysis" (рус. "за его фундаментальный вклад в вычислительную математику").
Еще совсем недавно операций с плавающей точкой, как и всех алгоритмов с вещественными числами, разработчики старались избегать. Сопроцессор, обрабатывающий операции с вещественными числами, был не на всех процессорах, а там, где был, не всегда работал эффективно. Но время шло, сейчас операции с плавающей точкой встроены в ядро процессора, мало того, видеочипы также активно обрабатывают вещественные числа, распараллеливая однотипные операции.
Куда уплывает точка
Не секрет, что вещественные числа процессор понимал не всегда. На заре эпохи программирования, до появления первых сопроцессоров вещественные числа не поддерживались на аппаратном уровне и эмулировались алгоритмически с помощью целых чисел, с которыми процессор прекрасно ладил. Так, тип real в старом добром Pascal был прародителем нынешних вещественных чисел, но представлял собой надстройку над целым числом, в котором биты логически интерпретировались как мантисса и экспонента вещественного числа.
Мантисса — это, по сути, число, записанное без точки. Экспонента — это степень, в которую нужно возвести некое число N (как правило, N = 2), чтобы при перемножении на мантиссу получить искомое число (с точностью до разрядности мантиссы). Выглядит это примерно так:
где m и e — целые числа, записанные в бинарном виде в выделенных под них битах. Чтобы избежать неоднозначности, считается, что 1 <= |m| < N, то есть число записано в том виде, как если бы оно было с одним знаком перед запятой, но запятую злостно стерли, и число превратилось в целое.
Мантисса — это, по сути, число, записанное без точки. Экспонента — это степень, в которую нужно возвести некое число N (как правило, N = 2), чтобы при перемножении на мантиссу получить искомое число.
Современные вещественные числа, поддержанные аппаратно на уровне процессора, также разбиты на мантиссу и экспоненту. Разумеется, все операции, привычные для арифметики целых чисел, также поддержаны командами процессора для вещественных чисел и выполняются максимально быстро.
Все так непросто потому, что такой формат записи, во‑первых, позволяет производить операции умножения и деления с такими числами достаточно эффективно, кроме того, получить исходное вещественное число, представленное таким форматом, также несложно. Данное представление чисел называется числом с плавающей точкой.
Стандарт точечного плаванья
Вещественные числа с плавающей точкой, поддержанные на уровне процессора, описаны специальным международным стандартом IEEE 754. Основными двумя типами для любых вычислений являются single-precision (одинарной точности) и double-precision (двойной точности) floating-point (числа с плавающей точкой). Названия эти напрямую отражают разрядность бинарного представления чисел одинарной и двойной точности: под представление с одинарной точностью выделено 32 бита, а под двойную, как ни странно, 64 бита — ровно вдвое больше.
Кроме одинарной и двойной точности, в новой редакции стандарта IEEE 754—2008 предусмотрены также типы расширенной точности, четверной и даже половинной точности. Однако в C/C++, кроме float и double, есть разве что еще тип long double, упорно не поддерживаемый компанией Microsoft, которая в Visual C++ подставляет вместо него обычный double. Ядром процессора в настоящий момент также, как правило, пока не поддерживаются типы половинной и четверной точности. Поэтому, выбирая представления с плавающей точкой, приходится выбирать лишь из float и double.
В качестве основания для экспоненты числа по стандарту берется 2, соответственно, приведенная выше формула сводится к следующей:
Расклад в битах в числах одинарной точности выглядит так:
1 бит под знак | 8 бит экспоненты | 23 бита мантиссы |
---|
Для двойной точности мы можем использовать больше битов:
1 бит под знак | 11 бит экспоненты | 52 бита мантиссы |
---|
В обоих случаях если бит знака равен 0, то число положительное и 1 устанавливается для отрицательных чисел. Это правило аналогично целым числам с той лишь разницей, что в отличие от целых, чтобы получить число, обратное по сложению, достаточно инвертировать один бит знака.
Поскольку мантисса записывается в двоичном виде, подразумевается целая часть, уже равная 1, поэтому в записи мантиссы всегда подразумевается один бит, который не хранится в двоичной записи. В битах мантиссы хранится именно дробная часть нормализованного числа в двоичной записи.
Мантисса записывается в двоичном виде, и отбрасывается целая часть, заведомо равная 1, поэтому никогда не забываем, что мантисса на один бит длиннее, чем в она хранится в двоичном виде.
Не нужно иметь докторскую степень, чтобы вычислить точность в десятичных знаках чисел, которые можно представить этим стандартом: 2 23 + 1 = 16 777 216; это явно указывает нам на тот факт, что точность представления вещественных чисел с одинарной точностью достигает чуть более семи десятичных знаков. Это значит, что мы не сможем сохранить в данном формате, например, число 123 456,78 — небольшое, в общем‑то, число, но уже начиная с сотой доли мы получим не то число, что хотели. Ситуация усложняется тем, что для больших чисел вида 1 234 567 890, которое прекрасно помещается даже в 32-разрядное целое, мы получим погрешность уже в сотнях единиц! Поэтому, например, в C++ для вещественных чисел по умолчанию используется тип double. Мантисса числа с двойной точностью уже превышает 15 знаков: 2 52 = 4 503 599 627 370 496 и спокойно вмещает в себя все 32-разрядные целые, давая сбой только на действительно больших 64-разрядных целых (19 десятичных знаков), где погрешность в сотнях единиц уже, как правило, несущественна. Если же нужна большая точность, то мы в данной статье обязательно в этом поможем.
Теперь что касается экспоненты. Это обычное бинарное представление целого числа, в которое нужно возвести 10, чтобы при перемножении на мантиссу в нормализованном виде получить исходное число. Вот только в стандарте вдобавок ввели смещение, которое нужно вычитать из бинарного представления, чтобы получить искомую степень десятки (так называемая biased exponent — смещенная экспонента). Экспонента смещается для упрощения операции сравнения, то есть для одинарной точности берется значение 127, а для двойной 1023. Все это звучит крайне сложно, поэтому многие пропускают главу о типе с плавающей точкой. А зря!
Примерное плаванье
Чтобы стало чуточку понятнее, рассмотрим пример. Закодируем число 640 (= 512 + 128) в бинарном виде как вещественное число одинарной точности:
- число положительное — бит знака будет равен 0;
- чтобы получить нормализованную мантиссу, нам нужно поделить число на 512 — максимальную степень двойки, меньшую числа, получим 640 / 512 = 512 / 512 + 128 / 512 или 1 + 1/4, что дает в двоичной записи 1,01, соответственно, в битах мантиссы будет 0100000 00000000 00000000;
- чтобы получить из 1 + 1/4 снова 640, нам нужно указать экспоненту, равную 9, как раз 2 9 = 512, число, на которое мы поделили число при нормализации мантиссы, но в бинарном виде должно быть представление в смещенном виде, и для вещественных чисел одинарной точности нужно прибавить 127, получим 127 + 9 = 128 + 8, что в бинарном виде будет записано так: 10001000.
Для двойной точности будет почти все то же самое, но мантисса будет содержать еще больше нулей справа в дробной части, а экспонента будет 1023 + 9 = 1024 + 8, то есть чуть больше нулей между старшим битом и числом экспоненты: 100 00001000.
В общем, все не так страшно, если аккуратно разобраться.
Задание на дом: разобраться в двоичной записи следующих констант: плюс и минус бесконечность (INF — бесконечность), ноль, минус ноль и число‑не‑число (NaN — not-a-number).
За буйки не заплывай!
Есть одно важное правило: у каждого формата представления числа есть свои пределы, за которые заплывать нельзя. Причем обеспечивать невыход за эти пределы приходится самому программисту, ведь поведение программы на С/С++ — это сделать невозмутимое лицо при выдаче в качестве сложения двух больших положительных целых чисел маленькое отрицательное. Но если для целых чисел нужно учитывать только максимальное и минимальное значение, то для вещественных чисел в представлении с плавающей точкой следует больше внимания обращать не столько на максимальные значения, сколько на разрядность числа. Благодаря экспоненте максимальное число для представления с плавающей точкой при двойной точности превышает 10 308 , даже экспонента одинарной точности дает возможность кодировать числа свыше 10 38 . Если сравнить с «жалкими» 10 19 , максимумом для 64-битных целых чисел, можно сделать вывод, что максимальные и минимальные значения вряд ли когда‑либо придется учитывать, хотя и забывать про них не стоит.
Если для целых чисел нужно учитывать только максимальное и минимальное значение, то для вещественных чисел в представлении с плавающей точкой следует больше внимания обращать не столько на максимальные значения, сколько на разрядность числа.
Соответственно, любые многомиллиардные суммы будут давать значительную погрешность в дробной части. При большой интенсивности обработки таких чисел могут пропадать миллиарды евро, просто потому, что они «не поместились», а погрешность дробной части суммировалась и накопила огромный остаток неучтенных данных.
Что же делать нам, программистам на C++, если перед нами стоит задача обработать числа очень большой разрядности, при этом не используя высокоуровневые языки программирования? Да то же, что и обычно: заполнить пробел, создав один небольшой тип данных для работы с десятичными дробями высокой точности, аналогичный типам Decimal высокоуровневых библиотек.
Добавим плавающей точке цемента
Пора зафиксировать плавающую точку. Поскольку мы решили избавиться от типа с плавающей точкой из‑за проблем с точностью вычислений, нам остаются целочисленные типы, а поскольку нам нужна максимальная разрядность, то и целые нам нужны максимальной разрядности в 64 бита.
Сегодня в учебных целях мы рассмотрим, как создать представление вещественных чисел с гарантированной точностью до 18 знаков после точки. Это достигается простым комбинированием двух 64-разрядных целых для целой и дробной части соответственно. В принципе, никто не мешает вместо одного числа для каждой из компонент взять массив значений и получить полноценную «длинную» арифметику. Но будет более чем достаточно сейчас решить проблему точности, дав возможность работать с точностью по 18 знаков до и после запятой, зафиксировав точку между двумя этими значениями и залив ее цементом.
Отсыпь и мне децимала!
Сначала немного теории. Обозначим наше две компоненты, целую и дробную часть числа, как n и f, а само число будет представимо в виде
x = n + f * 10 -18 , где n, f — целые, 0 <= f < 10 18 .
Для целой части лучше всего подойдет знаковый тип 64-битного целого, а для дробной — беззнаковый, это упростит многие операции в дальнейшем.
Такой метод является компромиссом между точностью и диапазоном представляемых значений. Представление чисел с плавающей точкой рассмотрим на примере чисел двойной точности (double precision). Такие числа занимают в памяти два машинных слова (8 байт на 32-битных системах). Наиболее распространенное представление описано в стандарте IEEE 754.
Кроме чисел двойной точности также используются следующие форматы чисел:
- половинной точности (half precision) (16 бит),
- одинарной точности (single precision) (32 бита),
- четверной точности (quadruple precision) (128 бит),
- расширенной точности (extended precision) (80 бит).
При выборе формата программисты идут на разумный компромисс между точностью вычислений и размером числа.
Недостатком такой записи является тот факт, что числа нельзя записать однозначно: [math] 0.01 = 0.001 \times 10^1 [/math] .
Число с плавающей точкой хранится в нормализованной форме и состоит из трех частей (в скобках указано количество бит, отводимых на каждую секцию в формате double):
- знак
- экспонента (показатель степени) (в виде целого числа в коде со сдвигом)
- мантисса (в нормализованной форме)
В качестве базы (основания степени) используется число [math] 2 [/math] . Экспонента хранится со сдвигом [math] -1023 [/math] .
Итоговое значение числа вычисляется по формуле:[math] x = (-1)^ \times (1.mant) \times 2^ [/math] .
- В нормализованном виде любое отличное от нуля число представимо в единственном виде. Недостатком такой записи является тот факт, что невозможно представить число 0.
- Так как старший бит двоичного числа, записанного в нормализованной форме, всегда равен 1, его можно опустить. Это используется в стандарте IEEE 754.
- В отличие от целочисленных стандартов (например, integer), имеющих равномерное распределение на всем множестве значений, числа с плавающей точкой (double, например) имеют квазиравномерное распределение.
- Вследствие свойства 3, числа с плавающей точкой имеют постоянную относительную погрешность (в отличие от целочисленных, которые имеют постоянную абсолютную погрешность).
- Очевидно, не все действительные числа возможно представить в виде числа с плавающей точкой.
- Точно в таком формате представимы только числа, являющиеся суммой некоторых обратных степеней двойки (не ниже -53). Остальные числа попадают в некоторый диапазон и округляются до ближайшей его границы. Таким образом, абсолютная погрешность составляет половину величины младшего бита.
- В формате double представимы числа в диапазоне [math] [2.3 \times 10^, 1.7 \times 10^] [/math] .
В нормализованной форме невозможно представить ноль. Для его представления в стандарте зарезервированы специальные значения мантиссы и экспоненты.
Согласно стандарту выполняются следующие свойства:
- [math] +0 = -0 [/math]
- [math] \frac< \left| x \right| >= -0\,\![/math] (если [math]x\ne0[/math] )
- [math] (-0) \cdot (-0) = +0\,\![/math]
- [math] \left| x \right| \cdot (-0) = -0\,\![/math]
- [math] x + (\pm 0) = x\,\![/math]
- [math] (-0) + (-0) = -0\,\![/math]
- [math] (+0) + (+0) = +0\,\![/math]
- [math] \frac<-\infty>= +0\,\![/math]
- [math] \frac<\left|x\right|>= -\infty\,\![/math] (если [math]x\ne0[/math] )
Для приближения ответа к правильному при переполнении, в double можно записать бесконечное значение. Так же, как и в случае с нолем, для этого используются специальные значение мантиссы и экспоненты.
Бесконечное значение можно получить при переполнении или при делении ненулевого числа на ноль.
В математике встречается понятие неопределенности. В стандарте double предусмотрено псевдочисло, которое арифметическая операция может вернуть даже в случае ошибки.
Неопределенность можно получить в нескольких случаях. Приведем некоторые из них:
- [math] f(NaN) = NaN [/math] , где [math] f [/math] - любая арифметическая операция
- [math] \infty + (-\infty) = NaN [/math]
- [math] 0 \times \infty = NaN [/math]
- [math] \frac<\pm0><\pm0>= \frac<\pm \infty><\pm \infty>= NaN [/math]
- [math] \sqrt = NaN [/math] , где [math] x \lt 0 [/math]
Денормализованные (denormalized numbers) - способ увеличить количество представимых числе в окрестности нуля. Каждое такое число по модулю меньше самого маленького нормализованного.< Согласно стандарту числа с плавающей точкой можно представить в следующем виде:
- [math] (-1)^s \times 1.M \times 2^E [/math] , в нормализованном виде если [math] E_ \leq E \leq E_ [/math] ,
- [math] (-1)^s \times 0.M \times 2^E_ [/math] , в денормализованном виде если [math] E = E_ - 1 [/math] ,
где [math] E_ [/math] - минимальное значение порядка, используемое для записи чисел (единичный сдвиг), [math] E_ - 1 [/math] - минимальное значение порядка, которое он может принимать - все биты нули, нулевой сдвиг.
Ввиду сложности, денормализованные числа обычно реализуют на программном уровне, а не на аппаратном. Из-за этого резко возрастает время работы с ними. Это недопустимо в областях, где требуется большая скорость вычислений (например, видеокарты). Так как денормализованные числа представляют числа мало отличные от нуля и мало влияют на результат, зачастую они игнорируются (что резко повышает скорость). При этом используются две концепции:
- Flush To Zero (FTZ) - в качестве результата возвращается нуль, как только становится понятно, что результат будет представляться в денормализованном виде.
- Denormals Are Zero (DAZ) - денормализованные числа, поступающие на вход, рассматриваются как нули.
Начиная с версии стандарта IEEE 754 2008 года денормализованные числа называются "субнормальными" (subnormal numbers), то есть числа, меньшие "нормальных".
Таким образом, компьютер не различает числа [math] x [/math] и [math] y [/math] , если [math] 1 \lt \fracМера единичной точности используется для оценки точности вычислений.
Приведем пример кода на Python, который показывает, при каком значении числа [math] x [/math] компьютер не различает числа [math] x [/math] и [math] x + 1 [/math] .
То есть [math] x = 2^ [/math] , так как мантисса числа двойной точности содержит 53 бита (в памяти хранятся 52). В C++ для расчета расстояния между двумя числами двойной точности можно воспользоваться функцией [math] \mathrm
- [math] a \oplus b = (a + b) (1 + \delta), |\delta| \leq \varepsilon_m [/math] ,
- [math] a \ominus b = (a - b) (1 + \delta), |\delta| \leq \varepsilon_m [/math] ,
- [math] a \otimes b = ab (1 + \delta), |\delta| \leq \varepsilon_m [/math] .
[math] \exists \tilde \in D: [/math]
- [math] \tilde \gt \tilde \Rightarrow (b - a) \times (c - a) \gt 0 [/math]
- [math] \tilde \lt -\tilde \Rightarrow (b - a) \times (c - a) \lt 0 [/math]
Обозначим [math] v = (b - a) \times (c - a) = (b_x - a_x) (c_y - a_y) - (b_y - a_y) (c_x - a_x)[/math] .
Теперь распишем это выражение в дабловой арифметике.
[math]\tilde = (b_x \ominus a_x) \otimes (c_y \ominus a_y) \ominus (b_y \ominus a_y) \otimes (c_x \ominus a_x) = \\ = [ (b_x - a_x) (c_y - a_y) (1 + \delta_1) (1 + \delta_2) (1 + \delta_3) - \\ - (b_y - a_y) (c_x - a_x) (1 + \delta_4) (1 + \delta_5) (1 + \delta_6) ] (1 + \delta_7),[/math]
[math] |\delta_i| \leq \varepsilon_m [/math]
Заметим, что [math] v \approx \tilde [/math]
Теперь оценим абсолютную погрешность [math] \epsilon = |v - \tilde|. [/math]
[math] |v - \tilde| = |(b_x - a_x) (c_y - a_y) - (b_y - a_y) (c_x - a_x) - \\ - (b_x - a_x) (c_y - a_y) (1 + \delta_1) (1 + \delta_2) (1 + \delta_3) (1 + \delta_7) + \\ + (b_y - a_y) (c_x - a_x) (1 + \delta_4) (1 + \delta_5) (1 + \delta_6) (1 + \delta_7)| = \\ = |(b_x - a_x) (c_y - a_y) (1 - (1 + \delta_1) (1 + \delta_2) (1 + \delta_3) (1 + \delta_7)) - \\ - (b_y - a_y) (c_x - a_x) (1 - (1 + \delta_4) (1 + \delta_5) (1 + \delta_6) (1 + \delta_7))| \leq \\ \leq |(b_x - a_x) (c_y - a_y) (1 - (1 + \delta_1) (1 + \delta_2) (1 + \delta_3) (1 + \delta_7))| + \\ + |(b_y - a_y) (c_x - a_x) (1 - (1 + \delta_4) (1 + \delta_5) (1 + \delta_6) (1 + \delta_7))| = \\ = |(b_x - a_x) (c_y - a_y)| \cdot |((1 + \delta_1) (1 + \delta_2) (1 + \delta_3) (1 + \delta_7) - 1)| + \\ + |(b_y - a_y) (c_x - a_x)| \cdot |((1 + \delta_4) (1 + \delta_5) (1 + \delta_6) (1 + \delta_7) - 1)| = \\ = |(b_x - a_x) (c_y - a_y)| \cdot |\delta_1 + \delta_2 + \delta_3 + \delta_7 + \delta_1 \delta_2 \ldots| + \\ + |(b_y - a_y) (c_x - a_x)| \cdot |\delta_4 + \delta_5 + \delta_6 + \delta_7 + \delta_4 \delta_5 \ldots| \leq \\ \leq |(b_x - a_x) (c_y - a_y)| \cdot (|\delta_1| + |\delta_2| + |\delta_3| + |\delta_7| + |\delta_1 \delta_2| \ldots) + \\ + |(b_y - a_y) (c_x - a_x)| \cdot (|\delta_4| + |\delta_5| + |\delta_6| + |\delta_7| + |\delta_4 \delta_5| \ldots) \leq \\ \leq |(b_x - a_x) (c_y - a_y)| \cdot (4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4) + \\ + |(b_y - a_y) (c_x - a_x)| \cdot (4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4) = \\ = (|(b_x - a_x) (c_y - a_y)| + |(b_y - a_y) (c_x - a_x)|)(4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4)[/math]
Пусть [math] t = (|(b_x - a_x) (c_y - a_y)| + |(b_y - a_y) (c_x - a_x)|).[/math] Получаем, что
[math] \epsilon = |v - \tilde| \leq t \cdot (4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4). [/math]
[math]\tilde = (|(b_x - a_x) (c_y - a_y) (1 + \delta_1) (1 + \delta_2) (1 + \delta_3)| + \\ + |(b_y - a_y) (c_x - a_x) (1 + \delta_4) (1 + \delta_5) (1 + \delta_6)|) (1 + \delta_7) \geq \\ \geq |(b_x - a_x) (c_y - a_y) (1 - \varepsilon_m)^3)|(1 - \varepsilon_m) + \\ + |(b_y - a_y) (c_x - a_x) (1 - \varepsilon_m)^3)|(1 - \varepsilon_m) = \\ = |(b_x - a_x) (c_y - a_y)| (1 - \varepsilon_m)^4 + |(b_y - a_y) (c_x - a_x)| (1 - \varepsilon_m)^4 = \\ = (|(b_x - a_x) (c_y - a_y)| + |(b_y - a_y) (c_x - a_x)|) (1 - \varepsilon_m)^4 = t \cdot (1 - \varepsilon_m)^4[/math]
[math] t \leq \tilde \frac <(1 - \varepsilon_m)^4>= \tilde (1 + 4 \varepsilon_m + 10 \varepsilon_m^2 + 20 \varepsilon_m^3 + \cdots) [/math]
[math] \epsilon = |v - \tilde| \leq \tilde \leq \tilde (1 + 4 \varepsilon_m + 10 \varepsilon_m^2 + 20 \varepsilon_m^3 + \cdots) (4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4) [/math]
[math] \tilde \lt 8 \varepsilon_m \tilde [/math]
Заметим, что это довольно грубая оценка. Вполне можно было бы написать [math] \tilde \lt 4.25 \varepsilon_m \tilde [/math] или [math] \tilde \lt 4.5 \varepsilon_m \tilde.[/math]
Читайте также: