Плавающая точка в .NET - часть 1: принципы и форматы

ОГЛАВЛЕНИЕ

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

Обзор

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

Целью данной серии из трех статей является раскрыть математику, стоящую за плавающими точками, показать то, почему они так важны для большинства программ, а также продемонстрировать то, как можно эффективно использовать их при программирование на платформе .NET. В первой части мы затронем основные принципы численных методов: численные форматы, точность и достоверность, погрешность округления. Мы также рассмотрим типы плавающих точек .NET в больших деталях. Вторая часть перечислит некоторые ловушки, связанные с численными методами, а также то, как их избежать. В третьей и последней части мы покажем то, как Microsoft обработал данную область в общеязыковой исполняющей среде (Common Language Runtime) и библиотеке базовых классов .NET (Base Class Library).



Введение

А вы можете сказать, что будет выведено после выполнения следующих строк? Мы подсчитываем деление на 103 в обоих случаях с одинарной и двойной точностью. Затем мы умножаем опять на 103, и сравним результаты с начальным значением:

Console.WriteLine("((double)(1/103.0))*103 < 1 is {0}.", ((double)(1/103.0))*103 < 1); Console.WriteLine("((float)(1/103.0F))*103 > 1 is {0}.", ((float)(1/103.0F))*103 > 1);

В точной арифметике, левые стороны сравнения равны 1, и потому ответ будет false (ложь) в обоих случаях. Но в обоих случаях будет выведено true (истина). Но это не все - мы получим результаты, которые не соответствуют тому, что мы ожидали с математической точки зрения. Два альтернативных способа выполнения этого подсчета дают противоречивые результаты!

Пример типичен для странной логики арифметических операций с плавающей точкой, которые не обладают лучшей репутацией. Вы встретите такое поведение во многих ситуациях. Без должной обработки ваши результаты будут неожиданными, а может даже нежеланными. К примеру, допустим, цена будет установлена в $4.99. И вы хотите узнать цену 17 единиц. Вы можете подсчитать так:

float price = 4.99F;
int quantity = 17;
float total = price * quantity;
Console.WriteLine("Общая цена равна ${0}.", total);

Вы будете ожидать результата в виде $84.83, но вы получите $84.82999. Если вы не будете осторожны, то вам это будет стоить денег. Допустим вы продаете товар за $100, и при этом предоставляете 10% скидку. Все ваши цены исчисляются целыми числами, и потому вы используете переменные типа int для хранения количества. Вот что вы получите:

int fullPrice = 100;
float discount = 0.1F;
Int32 finalPrice = (int)(fullPrice * (1-discount));
Console.WriteLine("The discounted price is ${0}.", finalPrice);

А теперь представьте, что финальная цена будет $89, а не $90 как ожидалось. Ваши клиенты будут рады, вы даете им еще 1%-ую скидку.

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



Некоторая терминология

Перед тем, как мы что-либо сделаем, мы должны определить термины, которые свойствены численным методам.

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

Компьютерная программа - это модель чего-то в реальном мире. Множество вещей в реальном мире представлено числами. Эти числа должны также быть представлены в компьютерных программах. Отсюда и были введены численные форматы.

Со стороны программиста, числовой формат - это набор чисел. В 99.9% случаев двоичное или внутреннее представление не настолько важно. Это может быть важным в том случае, если у нас битовые поля, но это нас не очень волнует. Что важно, так это то, что так можно представить числа из реального мира, который мы моделируем. Некоторые числовые форматы включают в себя специальные значения, которые обозначают неверные значения или те значения, которые находятся за пределами данного числового формата.

Целые числа

Большинство чисел являются целыми и их легко представить. Практически любое целое число, которое вы встретите может быть представлено "32-битным целым числом со знаком", что является числом в пределах от -2,147,483,648 до 2,147,483,647. Для некоторых приложений, таких, которые подсчитывают количество людей к примеру, вам понадобится более широкий формат - 64-битный тип целых чисел. Его область настолько широка, что можно подсчитать каждую десятую доли микросекунды за несколько тысячелетий. (Таким образом представлено значение DateTime.)

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

Числа с фиксированной запятой

Число с фиксированной точкой формируется путем умножения целого числа (мантисса) на какой-нибудь небольшой масштабный множитель, зачастую это отрицательная степень 10 или 2. Название получено исходя из того факта, что десятичная точка находится в фиксированной позиции при выводе числа. Примером формата фиксированной точки будет тип Currency в .NET Visual Basic и тип Money в SQL Server. Данные типы имеют область равную +/-900 биллонам и четыре цифры за десятичной точкой. Множитель равен 0.0001 и каждое умножение 0.0001 в пределах указанной области представлено таким форматом. Другим примером будет NTP-протокол (Network Time Protocol), где смещение времени возвращено в виде 32- и 64- битовых значений с фиксированной точкой, при этом 'двоичная' точка расположена в 16-ом и 32-ом битах соответственно.

Фиксированная точка пригодна для многих приложений. Для финансовых подсчетов это предоставило возможность представления таких чисел как 0.1 и 0.01 точно с верным множителем. Тем не менее, это не пригодно для многих приложений, где необходим более широкий ряд чисел. В физике микрочастиц часто используются числа меньшие чем 10-20, а специалисты по космологии оценивают число частиц в наблюдаемой вселенной примерно равное 1085. Непрактично представлять такие числа в виде формата с фиксированной точкой. Для того чтобы покрыть всю область, одно число должно занимать как минимум 50 байт!

Числа с плавающей запятой

Данная проблема была решена с введением чисел с плавающей точкой. Числа с плавающей точкой имеют фактор переменной масштаба, который указывается в качестве экспоненты степени небольшого числа, называемой базой, которая зачастую равна 2 или 10. .NET Framework определяет три типа плавающей точки: Single (одинарный), Double (двойной) и Decimal (десятичный). Все верно - десятичный тип (Decimal) не использует формат фиксированной точки, как Currency и Money. Он использует десятичный формат с плавающей точкой.

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

До того, как мы перейдем к изучению реальных форматов, нам необходимо определить еще парочку терминов.



Области и точность

Область числовых форматов - это интервал между наименьшими и наибольшими возможными значениями формата. Область 16-битного целого типа со знаком начинается с -32768 и завершается 32767. Область чисел с плавающей точкой двойной точности (практически) равна от -1e+308 до 1e+308. Числа за пределами области формата не могут быть представлены напрямую. Числа в пределах области могут не существовать в используемом числовом формате - это относится практически к большинству. Но всегда есть какой-нибудь числовой формат, который будет очень близок к необходимому числу.

Точность и достоверность зачастую считаются одним и тем же, но на самом деле это разные вещи.

Точность - это свойство числового формата, которое относится к количеству информации, используемой для представления числа. Лучшая или более высокая точность означает, что больше цифр может быть представлено, а также означает лучшее решение: числа представлены с высокой точностью и ближе к реальному значению. 1.3333 является числом, которое представлено с точностью в 5 десятичных цифр: одна до точки и четыре после. 1.333300 является тем же самым числом, но представленным с точностью в 7 чисел.

Точность может быть абсолютной и относительной. Целочисленные типы имеют абсолютную точность равную 1. Каждое целое число в пределах области типа представлено с такой точностью. Типы с фиксированной точкой, такие как Currency в ранних версиях Visual Basic, также обладают абсолютной точностью. Для типа Currency , она равна 0.0001, что означает, что каждое число, кратное 0.0001 в пределах области данного типа может быть представлено в данном формате.

Форматы с плавающей точкой используют относительную точность. Это означает, что точность всегда относительно зависима от размера числа. К примеру, 1.3331, 1.3331e+5 = 13331, и 1.3331e-3 = 0.0013331 все имеют 5 десятичных цифр относительной точности.

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

Использование точности в одну цифру:
0.4 * 0.6 + 0.6 * 0.4 = 0.24 + 0.24 Подсчет товара
= 0.2 + 0.2 Округление до 1 цифры после запятой
= 0.4 Финальный результат
Использование точности в две цифры:
0.4 * 0.6 + 0.6 * 0.4 = 0.24 + 0.24 Подсчет товара
= 0.24 + 0.24 Сохранение двух чисел
= 0.48 Подсчет суммы
= 0.5 Округление до 1 цифры после запятой

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

Достоверность - это свойство числа в определенном контексте. Оно отображает то, насколько число близко к истинному значению в данном контексте. Без контекста достоверность не имеет смысла, точно также, как "ему 25 лет" не имеет значения потому, что мы не знаем о ком идет речь.

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

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

Byte   n0 = 0x03;
Int16  n1 = 0x0003;
Int32  n2 = 0x00000003;
Single n3 = 3.000000f;
Double n4 = 3.000000000000000;

Каждая из данных пяти переменных точно представляет число 3. Переменные хранятся в различных видах точности, используя от 8 до 64 бит. Для ясности картины, точность чисел явно указана, но это никак не влияет на достоверность.

А теперь давайте посмотрим на то же число 3 в качестве приближенного значения для Пи (3.13). 3 всего лишь точно относительно десятичной части, независимо от точности. Значение типа Double использует в 8 раз больше памяти чем значение типа Byte value, но это никак не отразится на точности.



Погрешность округления

Допустим у вас не целое число, которое вы хотите использовать в своей программе - скорее всего вы встретите некоторые проблемы. Если число не имеет какой либо определенной формы, то оно не может быть отображено в каком-либо доступном числовом формате. Вашим единственным возможным решением будет нахождение числа, которое представлено числовым форматом наиболее близким к вашему числу. На протяжении жизни программы, вы будете использовать данное приближение в качестве вашего настоящего числа. Вместо использования точного значения a, программа будет использовать значение a+e, где e является очень малым числом, которое может быть как положительным, так и отрицательным. Это число e называется погрешностью округления.

И так не очень хорошо то, что вам приходится использовать приблизительное число, но все гораздо хуже. Практически в каждой арифметической операции в вашей программе результат данной операции также не будет представлен в числовом формате. Поверх первичной ошибки округления, практически каждая арифметическая операция имеет свою погрешность ei. К примеру, сложение двух чисел a и b, в результате даст число (a + b) + (ea + eb + esum), где ea, eb, и esum являются ошибками округления a, b, и результата соответственно. Ошибка округления растет, увеличивается с каждой операцией. К счастью, ошибки округления могут взаимно уничтожить друг друга в какой-то степени, но очень редко это происходит полностью. Некоторые операции также могут быть подвержены большей ошибке, чем другие.

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

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

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

К счастью, с появлением персонального компьютера все изменилось. К середине 80-х годов появился стандарт, который привнес некоторый порядок- стандарт IEEE-754 для двоичных арифметических устройств с плавающей запятой. Intel использовал это в разработке числового сопроцессора 8087. Некоторые признаки старой анархии все еще существовали некоторое время. Microsoft продолжала использовать свой бинарный формат ('Microsoft Binary Format') для чисел с плавающей точкой в своих интерпретаторах BASIC вплоть до представления QuickBasic 3.0.

Стандарт IEEE-754 затем стал более известен как "IEC 60559:1989, стандарт формата представления чисел с плавающей запятой для микропроцессоров". Он является официальным стандартным образцом, используемым компанией Microsoft во всех нынешних спецификациях.

Стандарт IEC 60559 не просто определяет числовые форматы. Он устанавливает направляющие для множества аспектов арифметических операций с плавающей точкой. Многие из данных стандартов являются обязательными, а некоторые являются опциональными. В третьей части мы рассмотрим реализацию стандарта компании Microsoft в общеязыковой среде исполнения (Common Language Runtime) и почему она не полна. Пока мы сконцентрируемся на двух числовых форматах, поддерживаемых CLR: числа с плавающей точкой с одинарной и двойной точностью. Мы также рассмотрим расширенный форма, для которого стандарт IEC 60559 определяет минимальные спецификации, и которые используются для единиц с плавающей точкой на процессорах Intel, а также используются в CLR.



Формат плавающей точки с обычной точностью

Мы теперь рассмотрим детали форматов с одинарной и двойной точностью. Хотя вам не обязательно знать все в деталях, данная информация может помочь вам понять некоторые нюансы работы с числами с плавающей точкой. Для того, чтобы все было не так сложно, мы сначала рассмотрим формат с одинарной точностью. Формат с двойной точностью схож с первым, и мы поговорим о нем позже.

Нормализованные числа

Типичное двоичное число с плавающей точкой имеет вид s � (m / 2N-1) � 2e, где s является либо -1 или +1, m и e являются мантиссами или значимой частью и экспонента указывается после, а N является числом бит в значимой части, что является константой для конкретного числового формата. Для чисел с одинарной точностью, N = 24. Числа s, m и e упакованы в 32 бита. Представление описано в следующей таблице:

часть знак экспонента дробная часть
номер бита 31 23-30 0-22

Знак хранится в наиболее значимом бите. Значение 0 указывает на положительное значение, а 1 указывает на отрицательное.

Поле экспоненты является 8-битным незнаковым целым числом, называемым смещенной экспонентой. Она равна экспоненте e а также константе, названной смещенной, которая имеет значение равное 127 для чисел с одинарной точностью. Это означает то, что к примеру экспонента -44 будет храниться в виде -44+127= 83 или 01010011. Существует два зарезервированных значения экспоненты: 0 и 255. Причина будет скоро рассмотрена, а в результате наименьшая реальная экспонента будет -126, а наибольшая +127.

Числовой формат кажется слишком сложным: вы можете умножить m на 2 и вычесть 1 из e и получить то же число. Такая двусмысленность разрешается путем минимизации экспоненты, и увеличения размера значимой части. Процесс называется нормализацией. В результате, значимое число m всегда имеет 24 бита, где ведущий бит всегда равен 1. Поскольку оно всегда равно единице 1, нам не стоит хранить данный бит, и потому у нас значимая часть всегда занимает 23 из 24.

Другими словами, нормализация означает то, что число m / 2N-1 всегда расположено между 1 и 2. 23 хранимых бита также являются тем, что идет после десятичной запятой в случае, когда значимая часть делится на 2N-1. Поэтому данные биты иногда называются дробной частью.

Ноль и субнормальные числа

На данном этапе у вас может появиться вопрос о том, как хранится число ноль. Ведь ни m ни s не могут быть нулевыми, и потому ноль отобразить невозможно. Ответом будет то, что 0 является особенным числом с особым представлением. Более того представлений два!

Числа, которые мы пока описывали, чьи значимые части имеют наибольшую длину, называются нормализованными числами. Они представляют большинство чисел, выраженных в виде формата с плавающей точкой. Наименьшее положительное значение их будет 223 .2-126+1-24 = 1.1754e-38 , а наибольшее (224-1).2127+1-24 = 3.4028e+38.

Помните, что смещенная экспонента имеет два зарезервированных значения. Смещенная экспонента 0 используется для отображения числа ноль а также субнормальных и денормализированных чисел. Это такие числа, чья значимая часть не нормализована и их максимальная длина равна 23 битам. Сама экспонента равна -127+1-24=-149, что в результате является наименьшим положительным числом 2-149 = 1.4012e-45.

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

Бесконечности и не-числа (Not-a-Number)

Нам все же необходимо пояснить использование другой зарезервированной смещенной экспоненты со значением 255. Данная экспонента используется для представления бесконечностей и не-числовых значений.

Если смещенная экспонента имеет только единицы (то есть равна 255) и значимая часть выражена только нулями, то это будет бесконечностью. Знаковый бит показывает , если мы имеем дело с положительной или отрицательной бесконечностью. Данные числа возвращаются для операторов, которые или  не обладают конечным значением (как 1/0) или же они слишком велики для того, чтобы быть отображенными в виде нормализованного числа (то есть 21,000,000,000).

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

Если значимая часть отлична от нуля, то значение представляет собой не-число (Not-a-Number) или NaN. NaN могут быть двух видов: сигнальные и не сигнальные, или тихие, в зависимости от ведущего бита в значимой части (1 или 0 соответственно). Такое разделение не очень важно на практике, и может уже быть не использовано в последующих стандартах.

NaN производятся когда результаты подсчетов не существуют (к примеру, Math.Sqrt(-1) не является числом) либо же они не могут быть определены (бесконечность/бесконечность). Одной из особенностей NaN является то, что все арифметические операции, включающие NaN возвращают NaN, за исключением случая, когда результат будет таким же независимо от значения. К примеру, функция hypot(x, y) = Math.Sqrt(x*x+y*y) с x бесконечностью всегда равна положительной бесконечности, независимо от значения y. В результате, hypot(infinity, NaN) = бесконечность.

Также, любое сравнение NaN с любым другим числом, включая NaN, вернет "ложь". Исключением является оператор, который всегда возвращает "истина", даже тогда, когда сравниваемое значение также является NaN!

Значимая часть NaN может быть установлена в произвольное значение, иногда называемое грузом. Стандарт IEC 60559 указывает, что груз должен передаваться при подсчетах. К примеру, когда NaN добавляется к нормальному числу, к примеру 5.3, то результатом будет NaN с тем же грузом, что и первая операнда. Когда обе операнды NaN, то результирующий NaN будет содержать груз одной из операнд. Это позволяет передавать потенциально полезную информацию в NaN значениях. К сожалению, данная функциональность практически не используется.

Некоторые примеры

Давайте рассмотрим некоторые числа и их соответствующие битовые шаблоны.

Номер Знак Экспонента Мантисса
0 0 00000000 00000000000000000000000
-0 1 00000000 00000000000000000000000
1 0 01111111 00000000000000000000000
+Infinity 0 11111111 00000000000000000000000
NaN 1 11111111 10000000000000000000000
3.141593 0 10000000 10010010000111111011100
-3.141593 1 10000000 10010010000111111011100
100000 0 10001111 10000110101000000000000
0.000001 0 01101110 01001111100010110101100
1/3 0 01111101 01010101010101010101011
4/3 0 01111111 01010101010101010101011
2-144 0 00000000 00000000000000000100000

В частности посмотрите на поле экспоненты для 1 и 4/3. Оба числа находятся между 1 и 2, и потому их несмещенная экспонента равна нулю. Смещенная экспонента потому равна несмещенной экспоненте, то есть 127 или 1111111 в десятичном формате. Числа большие чем 2 имеют смещенную экспоненту больше чем 127. Числа меньшие чем 1 имеют смещенную экспоненту равную меньше чем 127.

Последнее число в таблице (2-144) денормализовано. Смещенная экспонента равна нулю, и поскольку 2-144 = 32*2-149 то доля равна 32 = 25.



Числа с двойной и повышенной точностью

Числа двойной точности с плавающей запятой хранятся в практически аналогичном виде, как и числа с одинарной точностью. Некоторые константы все же отличаются. Знак также занимает 1 бит - и это не удивительно. Смещенная экспонента занимает 11 бит, со смещенным значением 1023. Мантисса занимает 52 бита при этом 53-ий бит не явно установлен в 1 для нормализованных чисел.

Стандарт IEC 60559 не определяет конкретные значения для параметров расширенного формата с плавающей запятой - он только указывает минимальные значения. Расширенный формат использовался в процессорах Intel поскольку 8087 занимает 80 бит, при этом 15 бит идут на экспоненту и 64 бита на значащую цифру. В отличие от других форматов, расширенный формат не оставляет места для ведущего бита значащей цифры, что позволяет определенную оптимизацию процессоров и сохранения ценного места на чипе.

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

Формат Одинарная Двойная Повышенная
Длинна (биты) 32 64 80
Биты экспоненты 8 11 15
Exponent bias 127 1023 16383
Наименьшая экспонента -126 -1022 -16382
Наибольшая экспонента +127 +1023 +16383
Точность 24 53 64
Наименьшее положительное значение 1.4012985e-45 2.4703282292062327e-324 1.82259976594123730126e-4951
Наименьшее положительное нормализованное значение 1.1754944e-38 2.2250738585072010e-308 3.36210314311209350626e-4932
Наибольшее положительное значение 3.4028235e+38 1.7976931348623157e+308 1.18973149535723176502e+4932

Относительно десятичного формата

Десятичный тип (Decimal) в структуре .NET является нестандартным типом с плавающей запятой с основанием 10, при этом он занимает 128 бит. 96 из них используются для мантиссы. 1 бит используется для знака, а 5 бит для экспоненты, что может варьироваться от 0 до 28. Формат не следует какому-либо существующему или спланированному стандарту. Также не существует никаких бесконечностей и не чисел (NaN).

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



Другие части стандарта

В дополнение к числовым форматам, стандарт IEC 60559 также точно определяет поведение стандартных арифметических операций +, -, *, /, и квадратного корня.

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

  1. в направление к ближайшему числу (округление вниз или вверх, в зависимости от того, что даст меньшую ошибку)
  2. округление к нулю (округление вниз для положительных чисел и вверх для отрицательных)
  3. в направление к положительной бесконечности (всегда вверх)
  4. в направлении к отрицательной бесконечности (всегда вниз)

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

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

Исключение Ситуация Возвращаемое значение
Invalid operation (неверная операция) Операнда неверна для выполнения данной операции. NaN
Division by zero (деление на ноль) Попытка выполнить деление ненулевого значения на ноль. Бесконечность (1/-0 = отрицательная бесконечность)
Overflow (переполнение) Результат операции слишком велик для отображения в формате с плавающей точкой. Положительная или негативная бесконечность.
Underflow (ошибка обнуления) Результат операции слишком мал для отображения в формате с плавающей точкой. Положительный или отрицательный ноль.
Inexact (неточность) Результат округления операции не точен. Подсчитанное значение.

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

Исключения ведут себя также, как переполнения целых значений в CLR. По умолчанию никакие действия не применяются в случае переполнения. Тем не менее, в проверяемом контексте, исключения создаются при возникновении целочисленной перегрузки. Аналогично, IEEE-754/IEC 60559 определяет механизм передачи контроля специальному обработчику в случае возникновения исключения.



Реальный код

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

Мы будем придерживаться стандарта IEEE-754 и реализуем некоторые 'рекомендуемые функции', перечисленные в дополнении к стандартам для чисел с двойной точностью. Вот и они:

Функция Описание
CopySign(x, y) Копирует знак y в x.
Scalb(y, n) Подсчитывает y2n для целого значения n без подсчета 2n.
Logb(x) Возвращает несмещенную экспоненту x.
NextAfter(x, y) Возвращает значение следующего представимого соседа x со стороны y.
Finite(x) Возвращает "истину" если x является вещественным конечным числом.
Unordered(x, y) Возвращает "истину" если x и y не находятся в определенном порядке, то есть если либо x , либо y равны NaN.
Class(x) Возвращает класс плавающей точки числа x.

Мы не будем вдаваться в подробности, код легко понять. Но есть некоторые вещи, которые стоит обсудить.

Пробразование в и из двоичного представления

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

Для значений типа double, класс BitConverter содержит два полезных метода: DoubleToInt64Bits и Int64BitsToDouble. Как видно из названий, данные методы преобразуют double в форму 64-битного целого значения (integer). Не существует эквивалента для одинарных значений типа Single. К счастью, одной строкой небезопасного кода можно все исправить.

Нахождение следующего представимого соседа

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

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



Вывод

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

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

Автор: Jeffrey Sax

Загрузить исходный код - 8.92 Kb