Вы находитесь на странице: 1из 8

Числа с плавающей точкой

Арифметика приближенных чисел

Прошлый раз мы говорили о том, что при работе с компьютером мы работаем с


конечным множеством чисел, просто потому что размер переменных конечный. В
частности, это означает, что мы не можем записать бесконечную дробь, а потому в
общем случае операции с нецелыми числами неточны.
Возможно, вы сами сталкивались, или читали о случаях, когда при умножении
вроде на компьютере результат получался наподобие . Такой
результат получается потому что число не представляется точно в виде
двоичной дроби.
Под двоичной дробью имеется в виду дробь, как десятичная, только по
основанию два. Т.е. если десятичная дробь равна (одна
десятая две сотых три тысячных), то двоичная дробь равна
.
Такое свойство не уникально для двоичных дробей, конечно. Например, в
виде десятичной дроби равна и не представима в виде конечной
десятичной дроби. Не говоря уж о числах иррациональных, вроде или, тем
более, . Работа с действительными числами с помощью переменных конечного
размера принципиально не может быть точной.
Тот же эффект может привести к тому, что в то время как цикл
for (i = 1; i < 10; i++) {}
будет выполнен 9 раз, такой же на вид цикл
for (i = 0.1; i < 1; i += 0.1) {}
будет выполнен 10 раз (или наоборот с <= , зависит от реализации).
Рекомендация одной старой книги по программированию гласила: «10.0 раз по
0.1 едва ли будет равно 1.0» (книга «Элементы стиля программирования», Б.
Керниган, Ф. Плоджер).
И если получение на экране из примера выше – просто курьёз, то
от количества итераций цикла ход работы программы может зависеть существенно.
Работа с действительными числами на современных компьютерах
производится в математическом сопроцессоре. Раньше сопроцессор представлял
собой отдельную микросхему, затем стал помещаться в ту же микросхему, что и
основной процессор.
На одном из первых занятий по Альтернативным методам я спрашивал,
проходили ли студенты работу с числами с плавающей точкой на математическом
сопроцессоре, мне ответили – нет. Если вы проходили такой курс, тем проще вам
будет освоить сегодняшний материал.
Мы побеседуем о том, как представляются числа в математическом
сопроцессоре и о правилах арифметики приближённых чисел.
Итак, основной процессор умеет работать с целыми числами, но мы хотели бы
иметь возможность работать и с нецелыми, как нам представить нецелые числа в
компьютере?
Первая мысль следующая, Допустим, у нас есть переменные длиной 64 бита.
Мы можем отделить старшие 32 бита для целой части, а младшие – для дробной
(двоичной дроби как описано выше), т.е. мысленно поставить запятую дроби между
старшими и младшими 32 битами. О знаке пока говорить не будем для простоты.
Такое представление называется представлением чисел «с фиксированной
запятой» (или точкой). Сложение и вычитание таких чисел не отличается от
сложения и вычитания 64-битных целых, так что мы можем даже отчасти не менять
схемы работы с такими числами, просто считать, что между старшими и младшими
32 битами стоит запятая. С умножением не совсем так – возьмём для простоты 8-
битное число, если мы рассматриваем его как целое, то
, но если мы ставим посередине числа

запятую, то . А уж тем более сложнее с

делением, где при делении целого числа на целое может получаться дробное и даже
бесконечная дробь.
Что ж, для 64-битного числа с фиксированной запятой как описано выше мы
получаем число от 0 до примерно 4 миллиардов с точностью до одной
четырёхмиллиардной. Казалось бы, неплохо. Но во-первых, есть задачи, где нужны
числа большие и есть задачи, где нужны числа меньшие (если сами числа имеют
порядок одной миллиардной, работать с ними сколь-либо точно с точностью одной
четырёхмиллиардной не получится). А что ещё важнее – когда мы работаем с очень
большими числами, нам, как правило, не нужна точность до миллиардных, а когда
нам нужна большая точность, мы, как правило, и работаем с небольшими числами,
так что у нас будут теряться, не использоваться, либо старшие 32 бита, либо
младшие. Точнее, будет теряться не обязательно 32, а какое-то количество бит –
может, меньше, чем 32, а может, и больше, в зависимости от масштаба
используемых чисел.
Мало того, что при этом теряется память – памяти у компьютеров сейчас много,
хуже другое – теряется точность в смысле количества значащих цифр. Например,
обычно мы записываем число в виде - с помощью трёх цифр. Если бы у нас
была всего одна цифра для записи, нам пришлось бы записать его как и потерять
точность. Теперь представьте, что нам для чего-то нужно записать число ,и
пусть на этот раз у нас есть не одна цифра, а целых четыре – но с фиксированной
запятой, как в записи числе выше - . Нам придётся записать наше число как
- одной цифрой, нули в начале передают порядок числа, но не его значение в
данном порядке, с той же точностью в смысле количества значащих цифр, что и
выше, несмотря на то что у нас есть четыре цифры.
Но выход есть. Разберём на примере четырёх десятичных цифр, как в примере
выше. Давайте использовать наши четыре цифры иначе, вместо фиксированной
запятой после первой цифры отделим последнюю цифру для записи положения
запятой. Т.е. у нас будет три цифры для записи числа, а запятая у нас будет
«плавающей» - её место будет указываться четвёртой цифрой. Теперь мы сможем
записать любое число от до (т.е. до целого ) с одинаковой точностью
в смысле количества значащих цифр – от одной тысячной до почти тысячи, шесть
порядков с помощью всего четырёх цифр.
Более того, смотрите, мы использовали всего четыре положения запятой -
, , и , но число размером одну десятичную цифру может иметь
10 значений – от 0 до 9. Что означают другие значения нашего отделённого
одноциферного числа? Ну, это просто – положение запятой это, по сути, умножение
на 10 в степени:
Т.е. наше отделённое однозначное число содержит просто степень. Давайте,
чтобы можно было передать и большие числа, и малые, считать, что значение 5
нашего отделённого однозначного числа соответствует степени 0, тогда 4 будет
соответствовать степени , 3 - и т.д., и в другую сторону – 6 будет
соответствовать степени , 7 - и т.д.Тогда с помощью всего четырёх наших
цифр мы сможем передать числа от

(для значения отделённого числа 0) до

(для значения отделённого числа 9).


Огромный диапазон, и в каждой части этого диапазона мы передаём числа с
одной и той же точностью в смысле количества значащих цифр.
Но есть и свои недостатки. Для начала, мы можем передать числа и ,и
, но мы не можем сложить и , у нас нет способа записи числа
. Более того, мы работаем с числами вида , т.е.
с шагом , а с числами , т.е. с шагом
, - с разной точностью в разных диапазонах.
Ну ладно, мы выше говорили, что из-за равномерного шага во всех диапазонах,
у чисел с фиксированной запятой, значительное количество цифр переменной
теряется, т.к. при работе с большими числами не нужны малые дроби и наоборот, и
не используется для записи значащих цифр числа. Так что сочтём разный шаг в
разных диапазонах за приемлемый вариант.
Но есть и другие проблемы. Например, при , какая бы ни была степень,
получится ноль, так что мы получаем несколько (столько, сколько у нас значений
степени) комбинаций переменной для одного и того же числа 0, а это расточительно,
комбинации могли бы использоваться для разных чисел. Более того, одно и то же
число, например, 25, мы можем записать и как , и как (когда цифр
больше трёх – а в реальных компьютерах их, конечно, гораздо больше, количество
способов записи одного и того же числа ещё больше), что приводит не только к
потере используемых значений переменной, но и к сложностям при сравнении чисел
– нужно учитывать неоднозначность записи одного и того же числа.
И ещё хотелось бы исправить один нюанс, мы использовали три цифры, и сами
они, без степени, могли иметь значения от до , мы могли бы в следующих
версиях нашего компьютера использовать четыре цифры – и числа бы менялись до
, при каждом изменении числа цифр, без степени, менялся бы диапазон
значений, так что то же число, что раньше передавалось степенью 1 (например,
), теперь передавалось бы степенью 0, т.е. пришлось бы менять
алгоритмы работы с одними и теми же числами, что неудобно. Предпочтительно,
чтобы цифры без степени передавали только точность в смысле количества
значимых цифр, а диапазон задавался степенью.
Данный недостаток исправить достаточно просто – будем рассматривать наши
три цифры не как целое , а как (а 5 в отделённом однозначном числе будет
означать степень не 0, а 3), тогда при увеличении количества цифр будет
меняться точность, но сами цифры, без степени всегда будут представлять собой
число в диапазоне 0…1, а порядок мы всегда подгоним степенью,
Чтобы объяснить, как справляются с проблемой неоднозначности числа,
давайте посмотрим, из-за чего она возникает. Почему число 300 можно передать и
как , и как , и как ? Потому что ведущие нули (нули слева) не
считаются, и при умножении на 10 в степени просто сдвигаются. Если бы перед
подразумевалась какая-то цифра, например, - саму единицу при этом мы могли
бы нигде не хранить, она просто подразумевается – то числа , и
были бы, конечно, разными числами, неоднозначность бы исчезла. Также
как и, пример с учётом абзаца выше - , и . Для десятичной
системы есть вопрос с выбором ведущей, подразумеваемой цифры, но в двоичной
системе такого вопроса нет.
Именно такое представление чисел используется в сопроцессоре. Число в
сопроцессоре представляется в виде
,

где буквами мы обозначили цифры двоичной дроби – что такое двоичная дробь,
мы писали выше, единица перед точкой нигде не хранится, она просто
подразумевается, а степень хранится в отдельной части битов числа.
Для такой записи числа, часть, которую мы раньше обозначали , а здесь для
двоичного случая , называется мантиссой, а число, задающее степень, –
характеристикой.
Числа типа double хранятся в 64-битных ячейках и имеют следующее
распределение бит,

насчёт характеристики, как и мы выше, договорились определённое значение


принять за степень 0, меньшие – за отрицательные степени, большие – за
положительные.
Такие числа называются нормализованными числами с плавающей точкой. Они
позволяют передавать широчайший диапазон значений (числа типа double могут
иметь значение от приблизительно до , т.е. от до , плюс знак),
они однозначны и в любой части диапазона передают числа с одной и той же
точностью в смысле количества значащих цифр (для чисел типа double
приблизительно 16 десятичных цифр), кроме того, специальные комбинации бит
представления числа в сопроцессоре имеют значения и , а также - не
число.
Но есть у таких чисел и недостатки. Например, как вы сами можете легко
увидеть, никакой комбинацией и в записи нельзя передать
. Можно назвать нулём и при вычислениях считать за нуль какую-то комбинацию,
обычно соответствующую очень маленькому числу (для некоторых вычислений в
сопроцессоре вводится даже две такие комбинации, для и ), но строгий ноль
передать нельзя. Кроме того, для записи недоступны и числа, меньшие , где
под понимается наименьшее отрицательное значение степени.
Поэтому кроме нормализованных чисел с ведущей подразумеваемой единицей
сопроцессор использует и денормализованные числа с ведущим нулём, которые мы
вводили выше, при этом степень считается равной наименьшему своему
отрицательному значению, т.е. денормализованные числа используются только в
области наименьших чисел, где у нормализованных чисел возникают описанные
выше проблемы. Однозначность степени для денормализованных чисел снимает
проблему неоднозначности чисел с ведущим нулём, фактически превращая их в
числа с фиксированной запятой, только не посередине числа, а запятой далеко
слева, соответственно, у нормализованных чисел есть та же проблема, что у чисел с
фиксированной запятой – для малых значений значительная часть старших битов не
используется, т.е. для малых денормализованных чисел уменьшается точность в
смысле количества значащих цифр.
Мы познакомились с представлением чисел с плавающей точкой в компьютере.
Что касается работы с такими числами, то арифметика чисел плавающей точкой.
конечно, во многом похожа на арифметику действительных чисел, но имеет отличия.
Например, от перестановки слагаемых результат меняется. Правда, не для двух
чисел, но для большого числа слагаемых.
Простой пример на языке нашей десятичной аналогии с тремя цифрами .
Если мы сто раз сложим , получим , а затем прибавим результат к
, т.е. найдём
,

получим , но если мы сто раз прибавим к , т.е. найдём

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

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


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

Начнём с темы округления. Общие принципы округления вы помните со


школы. Если часть числа за тем разрядом, т.е. справа от того разряда, до которого
мы округляем, меньше половины от единицы данного разряда, цифры справа просто
отбрасываются, если больше половины от единицы – к разряду, до которого
происходит округление, прибавляется единица. Разряд – это единицы, десятки,
сотни… и десятые, сотые, тысячные… Т.е. если мы округляем до разряда сотен и
число справа меньше 50 (т.е. ближайшая цифра 4, 3, 2…) – цифры справа будут
отброшены, если больше 50 (т.е. либо ближайшая цифра 6, 7…, либо ближайшая
цифра 5, но вместе со следующей цифрой составляет 51, 52, 53…), то к разряду
сотен будет прибавлена единица. Под «отбрасыванием» имеется в виду замена
нулями, если речь о «целых» разрядах - слева от десятичной запятой, и просто
отбрасывание, если речь о разрядах после запятой.
Однако две вещи в школах обычно не обсуждаются.
1. Что делать, если число справа равно ровно половине единицы разряда, до
которого происходит округление? Т.е., например, если округление происходит до
десятков, справа ровно 5, если до тысяч, справа ровно 500, если до тысячных,
справа ровно 0,0005. Округление всегда вверх, т.е. прибавлять единицу, как обычно
для простоты предлагается, может привести к накоплению ошибки, поэтому для
реальной работы с приближёнными числами предлагается следующее правило:
если текущая цифра в разряде, до которого происходит округление, нечётная,
единица прибавляется (и цифра становится чётной), если чётная – не прибавляется.
Сопроцессор использует именно это соглашение, round-half-even.
2. При передаче приближённых чисел принято записывать все их точно
известные разряды, поэтому даже при округлении дроби, нули справа не
отбрасываются, чтобы показать, до какого разряда цифры известны. Т.е. число
при округлении до сотых будет записано как .
С другой стороны, нули целого числа справа считаются обычно незначащими,
т.е. если нам дано число , считается, что у нас есть две значащих цифры, а
десятки и единицы числа просто неизвестны. Однако в некоторых случаях нуль
справа у целого числа может быть и значащим, если вы получили его при
суммировании значащих цифр, например при суммировании и получится -
обе цифры значащие, или если вы берёте данные из таблиц, где все значения,
например 3-значные, т.е. значения в таблице вычислены до трёх значащих цифр, и
встречаете число с 0 в конце.
Сложение приближённых чисел. При сложении и вычитании приближённых
чисел оставляют только те разряды, которые точно известны в обоих числа.
Например,

С вычитанием – аналогично, оставляем те разряды, которые известны у обоих


чисел. И при сложении, и при вычитании в конце может получиться значащий нуль.
Основание такого правила понятно. Если число в примере выше дано нам
только до первой цифры после запятой, значит, второй цифры мы точно не знаем,
там может быть любая цифра, более того, при получении числа могла
получиться погрешность в большую сторону, т.е. реальное значение меньше, т.е. в
следующем разряде стоит «отрицательная цифра». Так что если бы мы записали по
правилам обычной арифметики ответ , мы не дали бы более точный ответ, мы
ввели бы читателя в заблуждение.
Умножение и деление приближённых чисел выполняется следующим
образом – выполняется обычное умножение или деление, а затем смотрят, у какого
из исходных чисел было меньше всего значащих цифр и сохраняют только столько
значащих цифр, сколько было у этого числа, при этом порядок числа не важен – и у
,иу ,иу две значащие цифры. Например,

а не , т.к. у первого числа нам известны только две значащие цифры. Основание
правила то же, что для сложения и вычитания – передавать реально известные
цифры числа, а не вводить в заблуждение. Можете убедиться в том, что правило
должно быть именно таким, добавив к выше вопросительный знак в качестве
неизвестной цифры - .
При возведении в степень сохраняется столько значащих цифр, сколько их
было у основания, , а не , причём последние два нуля здесь – не
значащие, .
Перечисленные правила относятся к окончательным вычислениям, в
промежуточных оставляют на одну цифру больше.
Просто и подробное изложение правил работы с приближёнными числами вы
можете прочесть в книге «Занимательная арифметика» Я. И. Перельмана.
При вычислении значений функций вроде и правила несколько
сложнее. Вычисляют так называемое число обусловленности (condition number) –
формула для данного числа зависит от функции, значение которой определяется,
затем, после выполнения вычисления функции по обычным правилам оставляют
количество значимых цифр результата, равное количеству значимых цифр
аргумента минус десятичный логарифм числа обусловленности.
В случае суммирования, когда, как для чисел с плавающей точкой
сопроцессора, точность может теряться за счёт округления на каждом шаге
вычислений, погрешность может накапливаться. С погрешностью, связанной с самим
конечным размером ячейки сделать ничего нельзя, но с накоплением погрешности –
можно. Для этого используется алгоритм Кэхэна. Суть его состоит в вычислении на
каждом шаге отдельно суммы, отдельно – в другой ячейке – погрешности и затем, в
конце, прибавления погрешности к сумме.
Наконец, отметим, что использование значащих и незначащих цифр – не
единственный способ работы с неточно заданными числами. Можно указывать
интервал, в котором лежит точное значение, и затем проводить вычисления не с
числами, а с интервалами. Такой способ вычислений называется интервальной
арифметикой. Как и в случае с приближёнными числами, правила работы с
интервальными числами похожи на правила обычной, точной арифметики, но имеют
отличия.

Задачи

1. Записать в форме нормализованного числа – указать мантиссу и степень*


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

а) , б) , в) , г)

* степень, а не характеристику, т.к. степень сдвинута относительно характеристики,


степень считается равной нулю при некой положительной характеристике

2. Допустим, мы используем 8-битные нормализованные числа – 4 бита


мантисса, 3 бита характеристика, от до , причём значение 3 считается
степенью 0, 1 бит знак. Каковы максимальное и минимальное положительные числа,
которые можно передать в такой записи, каков минимальный шаг изменения числа
при максимальной и минимальной характеристике?

3. Найти сумму и разность приближённых чисел, указать количество значащих


цифр у результата, правые нули целого числа в условии считать незначащими, у
дробей – значащими

а) , б) , в)

4. Найти произведение и частое приближённых чисел, указать количество


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

а) , б) , в)

5. В тексте обсуждается число итераций цикла


for (i = 0.1; i < 1; i += 0.1) {},
будет ли число итераций другим, если заменить данный цикл на похожий, тоже с
нецелым шагом,
for (i = 0.5; i < 5; i += 0.5) {}?
Если да, почему?

Вам также может понравиться