Найти в Дзене
Разумный мир

О разрядности, точности, машинных вычислениях и суровом реальном мире

Оглавление

С одной стороны, эта статья относится в архитектуре ЭВМ и связанным с ней вопросам. С другой, к математике и инженерным (иногда и научным) расчетам. А с третьей, даже к метрологии. При всем при том это статья совсем не является теоретической. Она практическая во всех смыслах.

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

В статье

Нескучная метрология. Расстояние до солнца, толщина волоса, и двойная точность
Разумный мир28 июля 2022

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

В статье не будет высшей математики, но будут примеры и пояснения.

В статье рассматривается только обычное представление чисел и обычная арифметика. Модулярная арифметика (система остаточных классов) сегодня не рассматривается! Это совершенно отдельная тема.

Машины и числа бывают разные

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

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

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

О машинном представлении чисел в ЭВМ уже есть статьи

О представлении чисел в ЭВМ. С самого начала
Разумный мир16 апреля 2022
О представлении чисел в ЭВМ. Продолжение о системах счисления и целых числах.
Разумный мир21 апреля 2022
О представлении чисел в ЭВМ. Когда нельзя считать точно. Рациональные числа
Разумный мир28 апреля 2022
О представлении чисел в ЭВМ. Когда запятая плавает
Разумный мир3 мая 2022

Кроме того, у меня есть и довольно подробное описание форматов представления данных в процессорах Intel x86

Однако, мы сегодня будем больше рассматривать не вопросы, как представлены данные, а почему именно так, и как с ними работать.

Целые числа

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

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

К целым числам неприменим термин "точность" (хоть одинарная, хоть двойная). Точность представления целых чисел неизменна. Но к целым числам применим термин "разрядность". Причем вне зависимости от системы счисления и формата представления. Под разрядностью следует понимать именно количество хранимых цифр целого числа.

Разрядность числа это максимальное количество цифр в представлении (записи) числа. Иллюстрация моя
Разрядность числа это максимальное количество цифр в представлении (записи) числа. Иллюстрация моя

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

Увеличение разрядности, количества цифр в представлении целого числа, увеличивает диапазон представимых чисел, но никак не влияет на точность их представления. Для двоичного представления целых чисел можно считать условно стандартными разрядности 8, 16, 32, 64 бита. Условно по той причине, что конкретная реализация машины или компилятора может позволять существовать и 4 битным числам (тетрада), и 24 битным, и другим.

В языке С целые числа соответствуют типам short int (или просто short), int, long int (или просто long), long long int (или просто long long). Кроме того, тип char является не только символьным, но и целочисленным. Количество разрядов в каждом типе определяется конкретной реализацией, поэтому нельзя считать, что char всегда 8 бит, int 16 бит, long 32 бита, и так далее.

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

Формат представления целого числа может быть не только двоичным, но и десятичным или символьным. И даже двоично-десятичным, тем самым BCD, причем как упакованным, так и не упакованным. Для десятичных чисел (на десятичных машинах) указывается количество десятичных цифр в числе. Для двоично десятичных одна десятичная цифра занимает 8 бит (неупакованная) или 4 бита (упакованная), в общем случае.

Кроме того, целое число может иметь знак или не иметь. Во втором случае число может быть только равным нулю или положительным. В языке С это можно указать с помощью квалификаторов signed (по умолчанию, если не указан) или unsigned. В других языках возможность указания, что число не имеет знака, может отсутствовать.

Не смотря на все эти тонкости и различия в различных вариантах реализаций, для целых чисел применимы общие правила:

  • Диапазон представимых чисел зависит от максимального количества допустимых цифр в числе. Вне зависимости от используемой системы счисления. Для расширения диапазона требуется увеличивать "разрядность", количество допустимых цифр в числе.
  • Точность представления целых чисел не зависит от разрядности и равна единице младшего значащего разряда. В большинстве случаев, это означает, что точность равна единице, вне зависимости от системы счисления. Исключением является машина Сетунь. Можно сказать, что целые числа представлены точно.
  • Можно выделить "естественные" разрядности представления целых чисел конкретной машины. Числа этих разрядностей машина может обрабатывать одной арифметической командой, за один раз. Числа с разрядностью превышающей естественную требуют использования нескольких арифметических команд выполняемых последовательно (обычно, в цикле). Например, для 32-разрядной двоичной машины естественными разрядностями будут 8, 16 и 32. Число с 24 разрядами будет обрабатываться как число состоящее из 3 частей по 8 разрядов, а число с 64 разрядами как число из двух частей по 32 разряда.

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

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

Действительные числа, представление в виде обыкновенных дробей

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

Дробные числа. Иллюстрация моя
Дробные числа. Иллюстрация моя

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

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

У представления действительных чисел как обыкновенных дробей есть неоспоримые преимущества:

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

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

Недостатки представления чисел в виде обыкновенных дробей:

  • При выполнении арифметических операций числа имеют тенденцию к росту значений числителя и знаменателя. Простое увеличение разрядности числителя и знаменателя не позволяет решить проблему. Необходимо выполнять "нормализацию", упрощение числа.
  • Нормализация, упрощение, числа требует нахождения наибольшего общего делителя (НОД) для числителя и знаменателя. А это сложная и долгая операция которой посвящено не мало трудов математиков.
  • Действительные числа в виде обыкновенных дробей очень редко представлены в процессорах на аппаратном уровне. В массовых процессорах я такого вообще не встречал. Следовательно, реализация хранения и обработки таких чисел будет программной.
  • Для современного человека гораздо более привычной и удобной формой записи дробных чисел является формат десятичных дробей. Именно он показан на последней иллюстрации справа внизу.

Увы, недостатки представления действительных чисел в виде обыкновенных дробей в машинных расчетах перевешивают. Тем не менее, такое представление чисел иногда используется. Но мы сегодня не будем рассматривать его подробнее. Мы сосредоточимся на представлении действительных чисел в виде десятичных дробей.

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

Представление дробных чисел в виде десятичных дробей с фиксированной запятой

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

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

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

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

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

Нам всех хорошо знакомы числа с фиксированной запятой и работа с ними. Например, цену товара мы указываем в рублях и копейках, при этом 1 рубль равен 100 копейкам. И цена может быть записана так

123 р 38 к ⇨ 123.38 р

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

123.38 р ⇨ 12338 к

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

Но использование фиксированной запятой не сводится только к финансовой сфере. В средней школе не редкость уточнения к условиям задачи, например, "вычисления проводить до третьего знака после запятой". Это тоже фиксированная запятая, хоть этот термин в средней школе и не упоминается.

В языке С нет стандартного формата чисел с фиксированной запятой. Но такой формат можно найти в COBOL или PL/I. На примере последнего и рассмотрим

Описание представления дtqcndbntkmyjuj числа в формате с фиксированной запятой в языке PL/I. Иллюстрация моя
Описание представления дtqcndbntkmyjuj числа в формате с фиксированной запятой в языке PL/I. Иллюстрация моя

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

Хорошо видно, что число с фиксированной запятой действительно можно считать специализированным вариантом использования целого числа. Просто часть цифр будет считаться дробной частью. В PL/I десятичные и двоичные числа отличаются своим внутренним представлением. Двоичные это действительно всем привычные двоичные числа со знаком. А вот десятичные числа представлены в виде упакованных BCD. И работа с десятичным числом более медленная, а занимает оно больше места, чем двоичное.

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

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

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

DECLARE A FIXED DECIMAL (10,2) INITIAL (0.00);

A = 1.00 / 6.00;

Переменная А получит значение 1.66, а не 1.67. И это нужно учитывать. Внимательные читатели могут обвинить меня в "подтасовке" фактов, так как число 1/6 может быть представлено лишь в виде бесконечной десятичной дроби и уже по определению не может быть представлено точно. Поэтому я приведу другой пример, причем описание переменной А не изменится

A = 8.00 / 500.00;

В результате, переменная А получит значение 0.01, а не 0.016 (точное значение). Цифра 6 "не влазит" в описанный программистом формат и отбрасывается, без округления.

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

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

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

В языке PL/I общее количество цифр в десятичном числе с фиксированной запятой не могло превышать 15. Но, теоретически, количество цифр может быть почти любым. Только количество цифр в дробной части должно быть меньше общего количества цифр в числе. На первый взгляд, это позволяет получать практически любую точность как представления, так и вычислений. На самом деле, все немного сложнее.

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

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

В результате, у нас значение переменной А не изменится. Почему? Думаю читатели и сами знают, что это совершенно очевидный результат. Для выполнения операции нам нужно, что бы количество цифр в дробной части и общее количество цифр в числах были одинаковыми для обоих операндов. Поэтому формат промежуточного представления будет FIXED DECIMAL(13,8). Оба слагаемых будут конвертированы в этот формат. Изменения показаны на иллюстрации красным цветом.

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

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

Этот пример, при всей своей простоте, показывает серьезный недостаток чисел с фиксированной запятой. Если в вычислениях могут участвовать числа значительно различающиеся по значению, как в нашем примере, придется описывать формат представления учитывающий все возможные значения. В нашем случае, достаточным будет (13,8). Это не только неэффективно, это может привести к невозможности использования чисел с фиксированной запятой, так как промежуточный формат будет невозможно реализовать, хотя все операнды имеют допустимый формат. Например, в случае PL/I, количество цифр промежуточных результатов может превысить допустимые 15.

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

12 пФ = 0.000000000012 Ф

Согласитесь, это неудобно. А если нужно представить заряд электрона? Или расстояние от Земли до Солнца в километрах?

У формата представления действительных чисел с фиксированной запятой есть привлекательные плюсы:

  • По сути, такое представление можно считать целым числом с отдельно указанным положением десятичной точки. Это позволяет легко реализовать работу с такими числами на машин поддерживающих только целочисленную арифметику.
  • Представление чисел с фиксированной запятой дает автоматическое "форматирование" внешнего (печатного) представления.
  • Такое представление является естественным для некоторых прикладных областей. Например, финансовой. Именно на такое применение и рассчитан COBOL и FIXED DECIMAL в PL/I.

Но у такого формата действительных чисел есть и ощутимые недостатки:

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

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

Представление действительных чисел в виде десятичных дробей с плавающей запятой

Вот мы и добрались до основной темы сегодняшней статьи. Но все, что мы рассматривали ранее было не зря, оно нам сейчас пригодится.

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

Я в очередной раз повторю свою старую иллюстрацию

Экспоненциальная форма записи чисел. Иллюстрация моя
Экспоненциальная форма записи чисел. Иллюстрация моя

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

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

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

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

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

12 пФ = 0.000000000012 Ф = 12Е-12 Ф = 1.2Е-11 Ф

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

Экспоненциальная форма записи позволяет избежать "подсчета нолей" в записи числа. Запись 12Е-12 является ненормализованной записью, так как целая часть не находится в диапазоне от 1 до 9. Поэтому мы выполнили нормализацию передвинув запятую влево и откорректировав порядок. Такая процедура нормализации гораздо проще, чем для представления действительных чисел в виде обыкновенных дробей, не требуется нахождение НОД.

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

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

В общем случае, машинное представление представление чисел с плавающей может быть самым разным, не обязательно стандартным. Стандартные форматы представления нужны при необходимости обмена данными, причем в двоичном формате, между разными машинами или системами. Описывать все возможные варианты не имеет смысла. Стандартом считается, например, IEEE-854 (ранее IEEE-754). Но, повторюсь, стандарт представления может быть и иным. Или представление может быть вообще не стандартным. Но общим является именно использование двух целых чисел. Не обязательно двоичных.

Тем не менее, что бы разговор был более конкретным, кратко посмотрим, как числа с плавающей запятой представлены в стандарте. Поскольку стандарт доступен лишь на платной основе, воспользуемся документом "Intel® 64 and IA-32 Architectures. Software Developer’s Manual. Volume 1"

Иллюстрация из документации Intel
Иллюстрация из документации Intel

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

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

Fraction это мантисса. Для чисел одинарной точности, float в языке С, мантисса занимает 24 бита (двоичных разряда). Самый старший бит всегда должен быть равным 1 (установлен) в нормализованном числе, поэтому его не хранят в явном виде. Для чисел двойной точности, double в языке С, мантисса занимает 52 бита. Это более, чем в два раза, превышает число разрядов в числе одинарной точности. Но не будем цепляться к этой небольшой неточности. Общее количество двоичных разрядов в числе одинарной точности (32) действительно в два раза меньше, чем в числе двойной точности (64).

Exponent это порядок. Он хранится со смещением, что бы избежать необходимости записи знака порядка в явном виде. Но для нас сегодня это неважно. В числе одинарной точности под порядок отводится 8 бит, а в числе двойной точности 11 бит. Разрядность порядка определяет не точность (разрешающую способность) представления числа, а диапазон представимых значений.

Но давайте обратим внимание на еще один формат, который на иллюстрации обозначен "Double Extended-Precision Floating-Point". Это специфичный для Intel FPU внутренний 80 битный формат. С некоторой долей условности, можно считать этот формат некоторым эквивалентом long double в языке С.

Самое интересное, что этот внутренний формат Intel FPU использует при выполнении всех арифметических операций. Причем независимо от формата операндов. В этом формате мантисса занимает 64 бита (старший разряд хранится уже в явном виде).

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

Еще раз отмечу, что это касается только конкретной машины - Intel FPU, сопроцессора плавающей запятой, который является частью (сегодня неотъемлемой) процессоров Intel x86. Причем без учета расширений SSE. для других процессоров все может быть совсем по другому. Особенно, если процессор не поддерживает числа с плавающей запятой на аппаратном уровне.

Число разрядов мантиссы кажется очень большим. Тем не менее, представление числа 1/6 имеет погрешность уже в 7 цифре дробной части для одинарной точности и в 16 цифре для двойной точности. Это неплохая точность, но отнюдь не запредельная.

Куда пропадает точность?

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

Но это еще далеко не все. Давайте вспомним, что в IEEE-854 представление чисел двоичное. И вспомним, что один десятичный разряд не может быть представлен целым количеством двоичных разрядов. Три двоичных разряда позволяют представлять десятичные числа от 0 до 7, а четыре уже от 0 до 15. Вы уже поняли, что преобразование систем счисления вносит свою погрешность. Насколько велика эта погрешность?

Давайте возьмем красивое число, например, 0.1. Точнее, 1.0е-01 в нормализованном виде. Это абсолютно точно представимое в десятичной системе счисления число. Однако, после преобразования в двоичную систему счисления FPU будет считать это число вот таким:

double: 1.0000000000000000555111512312578270211815834045410156250e-01

float: 1.000000014901161193847656250e-01

Не совсем то, что мы ожидаем. Погрешность появилась в восьмом разряде для одинарной точности и в семнадцатом для двойной. Просто число 1.0е-01 оказывается не представимым точно в двоичной системе счисления в стандартном формате с плавающей запятой.

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

Давайте будем далее рассматривать не двоичную, а десятичную машину. Просто что бы не загромождать примеры еще преобразованием систем счисления. Такая замена машин нам нисколько не помешает. Будем считать, что наша машина точно представляет 5 десятичных разрядов мантиссы и 2 десятичных разряда порядка. То есть, в нашей машине диапазон представимых чисел с плавающей запятой, по модулю, от 1.0000е-99 до 9.9999е+99.

А теперь попробуем найти сумму двух чисел 1.2345е+05 и 1.2345е-05. Оба числа представлены точно. Их мантиссы равны, но порядки разные. И мы столкнемся с той же самой проблемой, с которой сталкивались при сложении чисел с фиксированной запятой. А именно, мы должны выровнять положения запятых при сложении. В нашем случае, порядки слагаемых должны быть равны.

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

Попытка приведения порядков слагаемых. Иллюстрация моя
Попытка приведения порядков слагаемых. Иллюстрация моя

Мы видим, как стремительно теряет свою точность второе слагаемое. Но с первым слагаемым пока все в порядке. А ведь мы пока так и не сумели выровнять порядки. Мы можем продолжить изменять порядок первого слагаемого, но тогда и оно начнет терять свою точность. Что же делать?

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

Повышение количества цифр мантиссы не помогает. Иллюстрация моя
Повышение количества цифр мантиссы не помогает. Иллюстрация моя

В данном случае я не стал показывать порядки, поскольку они одинаковы и равны +00. Да, наши слагаемые теперь представлены точно, но вот результат не поместился даже в удвоенную разрядность.

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

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

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

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

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

Вспомните, во что превратилось число 0.1 при представлении с одинарной и двойной точностью. А теперь представьте, что вы хотите сравнить переменную, которая получила свое значение в результате вычислений, и константы 0.1. Например, вот так

double var;

if(var == (double)0.1) { .... }

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

Сравнивать нужно, примерно, так

double var;

double epsilon=1.0e-08;

if((var - (double)0.1) < epsilon) { .... }

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

Заключение

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

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

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

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

До новых встреч!

Наука
7 млн интересуются