Decimal c что это
Перейти к содержимому

Decimal c что это

  • автор:

Decimal c что это

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

В языке C# есть следующие базовые типы данных:

    bool : хранит значение true или false (логические литералы). Представлен системным типом System.Boolean

bool alive = true; bool isDead = false;
byte bit1 = 1; byte bit2 = 102;
sbyte bit1 = -101; sbyte bit2 = 102;
short n1 = 1; short n2 = 102;
ushort n1 = 1; ushort n2 = 102;
int a = 10; int b = 0b101; // бинарная форма b =5 int c = 0xFF; // шестнадцатеричная форма c = 255
uint a = 10; uint b = 0b101; uint c = 0xFF;
long a = -10; long b = 0b101; long c = 0xFF;
ulong a = 10; ulong b = 0b101; ulong c = 0xFF;
char a = 'A'; char b = '\x5A'; char c = '\u0420';
string hello = "Hello"; string word = "world";
object a = 22; object b = 3.14; object c = "hello code";

Например, определим несколько переменных разных типов и выведем их значения на консоль:

string name = "Tom"; int age = 33; bool isEmployed = false; double weight = 78.65; Console.WriteLine($"Имя: "); Console.WriteLine($"Возраст: "); Console.WriteLine($"Вес: "); Console.WriteLine($"Работает: ");

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

Имя: Tom Возраст: 33 Вес: 78,65 Работает: False

Использование суффиксов

При присвоении значений надо иметь в виду следующую тонкость: все вещественные литералы (дробные числа) рассматриваются как значения типа double . И чтобы указать, что дробное число представляет тип float или тип decimal , необходимо к литералу добавлять суффикс: F/f — для float и M/m — для decimal.

float a = 3.14F; float b = 30.6f; decimal c = 1005.8M; decimal d = 334.8m;

Подобным образом все целочисленные литералы рассматриваются как значения типа int . Чтобы явным образом указать, что целочисленный литерал представляет значение типа uint, надо использовать суффикс U/u , для типа long — суффикс L/l , а для типа ulong — суффикс UL/ul :

uint a = 10U; long b = 20L; ulong c = 30UL;

Использование системных типов

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

int a = 4; System.Int32 b = 4;

Неявная типизация

Ранее мы явным образом указывали тип переменных, например, int x; . И компилятор при запуске уже знал, что x хранит целочисленное значение.

Однако мы можем использовать и модель неявной типизации:

var hello = "Hell to World"; var c = 20;

Для неявной типизации вместо названия типа данных используется ключевое слово var . Затем уже при компиляции компилятор сам выводит тип данных исходя из присвоенного значения. Так как по умолчанию все целочисленные значения рассматриваются как значения типа int , то поэтому в итоге переменная c будет иметь тип int . Аналогично переменной hello присваивается строка, поэтому эта переменная будет иметь тип string

Эти переменные подобны обычным, однако они имеют некоторые ограничения.

Во-первых, мы не можем сначала объявить неявно типизируемую переменную, а затем инициализировать:

// этот код работает int a; a = 20; // этот код не работает var c; c= 20;

Во-вторых, мы не можем указать в качестве значения неявно типизируемой переменной null :

// этот код не работает var c=null;

Так как значение null, то компилятор не сможет вывести тип данных.

decimal в C#

decimal в C#

Всем доброго времени суток. На связи Алексей Гулынин. В данной совсем короткой статье я бы хотел рассказать про тип данных decimal в C#. Данный тип данных, в основном, используется в финансовых расчетах. Он имеет разрядность 127 бит, что позволяет ему представлять числа с точностью до 28 десятичных разрядов. Таким образом он может представлять десятичные значения без ошибок округления (для типов данных float и double характерны ошибки округления десятичных значений).

Значения переменных типа decimal обычно указываются с суффиксом M . Без данного суффикса эти переменные интерпретируются как обычные переменные с плавающей точкой, которые несовместимы с decimal .

Напишем пример, который считает сложные проценты:

static void Main(string[] args) < decimal start_sum = 100000.0M; decimal procent = 0.08M; int year = 5; Console.WriteLine("Начальные вложения: ", start_sum); for (int i = 0; i < year; i++) < start_sum = start_sum + (start_sum * procent); >Console.WriteLine("Сумма через лет: ", year, start_sum); Console.WriteLine("Нажмите клавишу для продолжения. "); Console.ReadLine(); >

В данной статье вы узнали про тип данных decimal в C#.

На связи был Алексей Гулынин, оставляйте свои комментарии, увидимся в следующих статьях.

Decimal числа. Отличия от float

После рассказа про float меня просили рассказать про Decimal. Узнаем же, что это за зверь, как он устроен внутри и как с ним работать. Итак, Decimal – это класс из стандартного модуля decimal. Он представляет собой число с плавающей точкой, как и float. Да, именно с плавающей, потому что некоторые, я слышал, думают, что это число с фиксированной точкой.

Однако, Decimal имеет ряд существенных отличий от float.

Цель

Тип Decimal создан, чтобы операции над рациональными числами в компьютере выполнялись также, как они выполняются людьми, как их преподают в школе. Иными словами, чтобы все-таки 0.1 + 0.1 + 0.1 == 0.3 . Из-за ошибок представления, float приводит к утере точности, и такие простые на первый взгляд равенства не выполняются. А это может быть критично в высокоточных научных вычислениях, и главное в сфере бизнеса и финансов!

Внутреннее устройство

float – реализован по стандарту IEEE-754 как число с плавающей запятой двойной точности (64 бита) на основании 2 . Реализация таких чисел заложена прямо в железо почти любого современного процессора. Поэтому float в Python работает примерно также, как и double в С, С++, Java и прочих языках. И имеет такие же ограничения и «странности». Так как поддержка float имеет аппаратный характер, то его быстродействие сравнительно велико.

Decimal – число с плавающей точкой с основанием экспоненты – 10 , отсюда и название (decima лат. – десятая часть, десятина).

Он реализован по стандарту IBM: General Decimal Arithmetic Specification Version 1.70 – 7 Apr 2009, который в свою очередь основан на стандартах IEEE. По поводу реализации, в исходниках CPython я нашел два варианта: на чистом Python и с помощью Си-библиотеки libmpdec. Обе реализации есть в кодовой базе, хотя в последних версиях Python 3 используется именно Си-версия, очевидно, она в разы быстрее! Видите букву Си?

Python 3.7.5 (default, Nov 13 2019, 14:05:23) [Clang 11.0.0 (clang-1100.0.33.12)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import decimal >>> help(decimal) Help on module decimal: NAME decimal - C decimal arithmetic module .

Поэтому первый важный вывод:

Хоть Decimal и написан на Си, он в разы медленнее, чем float, так как реализован программно, а float – аппаратно.

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

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

Decimal_Число = ±мантисса * 10 экcпонента

Мантисса и экспоненты – целые числа.

Помните, что мы не могли представить 0.1 в float с основанием 2? С основанием 10 – это элементарно:

0.1 = 1 * 10 -1 , и таким образом, 0.3 = 3 * 10 -1 = (1 + 1 + 1) * 10 -1 = 0.1 + 0.1 + 0.1

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

Decimal оперирует с числами с произвольной (задаваемой пользователем), но конечной точностью.

По умолчанию точность – 28 десятичных знаков.

Еще одно следствие того, что Decimal реализовано программно – то, что его можно на ходу настраивать, как угодно пользователю. Для этого есть контекст – объект, содержащий настройки для выполнения операций и флаги. Операции выполняемые в этом контексте следуют правилам, заданным в нем. В отличии от float, где все правила фиксированы на аппаратном или низшим программным уровнях. Настроить можно:

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

Флаги в контексте устанавливаются со стороны модуля decimal, если при последнем вычислении случился какой-то из сигналов. (Это отдельная тема, о ней потом.)

Сам же объект Decimal содержит знак, мантиссу (коэффициент перед экспонентой) и саму экспоненту (степень). Лишние нули в мантиссе на обрезаются, чтобы сохранять значимость числа ( 1.20 * 2.40 = 2.8800 ).

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

Поработаем с Decimal

Начинаем с импорта и посмотрим, каков контекст по умолчанию:

>>> from decimal import * >>> getcontext() Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])

Мы видим здесь, что точность 28 знаков, округление к ближайшему четному, пределы по экспоненте ±999999, capitals – это про заглавную Е при печати, clamp не будем трогать пока что, флаги все сброшены, а включенные ловушки – неправильная операция, деление на ноль, переполнение. Если ловушка включена, это значит, что при возникновении соответствующего сигнала будет брошено исключение. Если нет ловушки, то при сигнале будет только втихую установлен флаг. Я оставлю тему ловушек на следующую статью.

Создание Decimal

Создать Decimal можно из обычного целого числа, из float, из строки или кортежа. С обычным числом все просто – int представлены и так точно:

>>> Decimal(1) Decimal('1') >>> Decimal(-1) Decimal('-1') >>> Decimal(10002332) Decimal('10002332')

Из float – надо быть очень аккуратным. Потому что, float округляется внутри до ближайшего возможного, а Decimal не знает о ваших первоначальных намерениях, поэтому копирует содержимое float. К примеру, числа 0.1 в представлении float просто не существует. Python считывает 0.1 из кода как строку, потому ищет наиболее близкий к нему возможный float, а из него уже копируется содержимое в Decimal, как есть – уже с ошибкой:

>>> Decimal(0.1) Decimal('0.1000000000000000055511151231257827021181583404541015625')

Не рекомендуется создавать Decimal из float. В Decimal попадет уже неправильно округленное число. Создавайте Decimal из целых чисел, либо из строк!

Логически правильно создавать Decimal сразу из строки, избегая фазу с превращением его в float! Что есть в строке – попадет в Decimal. Может показаться, что это немного криво – хранить числах в строках, но теперь вы знаете о представлении двоичного float, и строки обретают реальный смысл.

>>> Decimal('0.1') Decimal('0.1') >>> Decimal('3.14') Decimal('3.14') >>> Decimal('1.2e+10') Decimal('1.2E+10') >>> Decimal('10_000_000_000') # c версии Python 3.6 можно подчеркивания Decimal('10000000000')

Можно строкой еще задавать бесконечности и NaN (не число). Примеры:

>>> Decimal('Inf') Decimal('Infinity') >>> Decimal('-Inf') Decimal('-Infinity') >>> Decimal('nan') Decimal('NaN')

Если использовать кортеж для конструирования Decimal, то он должен содержать три элемента:

  1. Знак, как число: 0 – это плюс, 1 – это минус.
  2. Кортеж из значащих цифр мантиссы
  3. Число – показатель экспоненты

Вообще кортеж для Decimal использует редко. Но вот вам пример:

>>> Decimal((0, (1, 2, 3, 4, 5), -1)) Decimal('1234.5') >>> Decimal((1, (7, 7, 7), 3)) Decimal('-7.77E+5')

Если число слишком большое, то будет сигнал – неправильная операция. А так как на этом сигнале ловушка по умолчание – то будет исключение:

>>> Decimal("1e9999999999999999999") Traceback (most recent call last): File "", line 1, in decimal.InvalidOperation: []

Точность представление Decimal задается исключительно длиной задающего числа (или длиной строки). Настройки точности и режимов округления из контекста в ступают в игру только во время совершения математических операций.

>>> с = Context(prec=3) # точность 3 >>> Decimal('5.643434231', c) # но число целиком сохраняется Decimal('5.643434231') >>> Decimal('5.643434231', c) * 2 # после операции уже применяется округление до нужной точности Decimal('11.287') >>> +Decimal('5.643434231', c) # трюк: унарный плюс применит контекст Decimal('5.6434')

Decimal отлично интегрирован в среду Python. Что касается математики, то с Decimal работают все привычные операции: сложение, вычитание, умножение, деление, возведение в степень и так далее.

Работайте с Decimal как с обычными числами: складывайте, вычитайте, умножайте, делите и прочее. Можете, миксовать их с целыми числами. Но не рекомендуется миксовать их с float.

>>> x = Decimal('1.2') >>> y = Decimal('2.3') >>> x + y Decimal('3.5') >>> x - y Decimal('-1.1') >>> x * y Decimal('2.76') >>> x / y Decimal('0.52174') >>> y // x # деление нацело Decimal('1') >>> y % x # остаток Decimal('1.1') >>> Decimal('2.2') * 2 Decimal('4.4') >>> Decimal('2.2') - 1 Decimal('1.2')

Дополнительно еще доступны некоторые математические функции:

>>> getcontext().prec = 10 # просто точность задали >>> Decimal(2).sqrt() # корень квадратный Decimal('1.414213562') >>> Decimal(2).ln() # логарифм натуральный Decimal('0.6931471806') >>> Decimal(100).log10() # логарифм десятичный Decimal('2')

А вот чисел π и e из коробки не завезли, потому что не ясно, какая точность вам нужна. Их можно взять из модуля math в виде float или задать вручную до нужной точности или на худой конец вычислить. Аналогично для тригонометрии и специальных функций: либо берите неточные значения из math, либо вычисляйте сами до нужной точности рядами Тейлора или другими методами с помощью примитивных операций. В документации есть примеры вычисления констант и функций.

Кстати, Decimal можно передавать как аргументы функций, ожидающих float. Тогда они будут преобразованы во float:

>>> math.sin(Decimal(1)) 0.8414709848078965

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

>>> Decimal('10.4266').quantize(Decimal('.01'), rounding=ROUND_DOWN) Decimal('10.42') >>> Decimal('10.4266').quantize(Decimal('.01'), rounding=ROUND_UP) Decimal('10.43') >>> Decimal('10.4266').quantize(Decimal('1.'), rounding=ROUND_UP) Decimal('11')

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

>>> x = Decimal('0.1') >>> x + x + x == Decimal('0.3') True

Можно сортировать списки Decimal, искать минимум и максимум. А также преобразовывать Decimal обратно в обычные типы int, float, str. Пример из документации:

>>> data = list(map(Decimal, '1.34 1.87 3.45 2.35 1.00 0.03 9.25'.split())) >>> max(data) Decimal('9.25') >>> min(data) Decimal('0.03') >>> sorted(data) [Decimal('0.03'), Decimal('1.00'), Decimal('1.34'), Decimal('1.87'), Decimal('2.35'), Decimal('3.45'), Decimal('9.25')] >>> sum(data) Decimal('19.29') >>> a, b, c = data[:3] >>> str(a) '1.34' >>> float(a) 1.34 >>> round(a, 1) Decimal('1.3') >>> int(a) 1

Но! Не все сторонние библиотеки поддерживают Decimal. Например, не получится использовать его для numpy!

Не все операции над Decimal абсолютно точные, если результат неточен, то возникает сигнал Inexact.

>>> c = getcontext() >>> c.clear_flags() >>> Decimal(1) / Decimal(3) Decimal('0.3333333333') >>> c.flags[Inexact] True

Выводы

Выбор между Decimal и float – это поиск компромисса с учетом условий конкретной задачи.

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

Сравнительная таблица

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

Еще

В этой статье я не осветил полностью вопросы:

  • Сигналы, флаги и ловушки
  • Обзор режимов округления
  • Управление контекстами
  • Контексты и многопоточность

Если сообществу будет интересно, то я продолжу тему. Голосование будет на канале!

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway ��

О реализации точного представления чисел или «где хранить деньги?»

«Где хранить деньги?» это шутливое название поста, периодически появляющегося на компьютерных форумах. Имеется в виду вопрос: в переменных какого типа следует хранить результаты вычислений финансово-экономических расчетов. Обычно такой вопрос возникает у программистов, впервые сталкивающихся с подобными расчетами. Интуитивно они понимают, что здесь округления нежелательны или недопустимы. Интересно, что при этом часто вспоминают легенды о хитрых программистах, сумевших разницу между точным и округленным значением финансовых расчетов перечислять на свой собственный счет. Правда, эти истории названы сказками еще в книге [1], написанной 50 лет назад, причем даже там речь шла лишь о ревизиях банковских систем, основаниями для которых (т.е. для ревизий) могли бы служить подобные истории.

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

Пример необходимости точных вычислений

Рассмотрим простейший пример: рассчитать выплату 13% с суммы 1 рубль 30 копеек. Это значение легко сосчитать на бумаге «столбиком» и притом точно: 16.9 копейки. Но если запрограммировать этот же расчет с использованием данных типа «double», то получится ответ вроде 1.68999999999999E-001 рубля, поскольку значение 0.169 точно представить в формате IEEE-754 нельзя. Вот тут и начинаются сомнения: не приведет ли ряд подобных вычислений к накоплению погрешности? Определение итоговой погрешности для нетривиальных расчетов само является нетривиальной математической задачей и лучший способ ее решения – не допускать погрешностей вообще. Но как это сделать? И какой тип данных должен быть, если тип «целый» не годится, а тип «float» обязательно даст погрешность?

Виды представления чисел

Может показаться, что автор ломится в открытую дверь. Есть же в языках типы для работы с «деньгами», например, тип DECIMAL в языке C#. Как следует из описания языка [2], «Тип значения Decimal подходит для финансовых вычислений, требующих большого количества значимых целых и дробных цифр и отсутствия ошибок округления. Избежать округления с помощью типа Decimal нельзя. Однако он позволяет минимизировать связанные с ним ошибки». Странно, требуется избежать ошибок округления и одновременно утверждается, что этого сделать нельзя.

Одной из причин затруднений в таких случаях является, на мой взгляд, не совсем четкая классификация представления чисел. Многие привыкли делить числа только на целые и действительные, хотя правильнее было бы вначале разделить их на точные и приближенные. При этом целые – это подмножество точных, а не все множество. В приведенном примере расчета «на бумаге» хотя и получились копейки с долями, т.е. нецелое значение, никакой потери точности не произошло. Тип DECIMAL в языке C# корректнее было бы назвать «модифицированным FLOAT», поскольку он содержит те же знак, мантиссу и порядок. И значения этого типа все же приближенные, хотя и с очень большим числом значащих цифр.

Аппаратная поддержка точных вычислений

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

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

Механизм вычислений без округлений был встроен, например, в язык PL/1. Этот язык изначально разрабатывался для применения и в области экономических расчетов, для чего он получил «наследство» от языка экономических задач Кобола. Можно даже предположить, что команды двоично-десятичной арифметики были вставлены в архитектуру IA-32 (точнее, еще в IA-16 в 1978 году) именно для поддержки в языках высокого уровня типа PL/1 аппарата точных вычислений. Поскольку автор сопровождает и использует транслятор языка PL/1 [3], дальнейшее изложение будет относиться к конкретной реализации. Сама реализация довольна проста и объем команд транслятора, обеспечивающих генерацию точных вычислений, не превышает 3-4% от размера транслятора.

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

Граница между представлениями чисел в PL/1 проведена корректно: все числа точного представления имеют тип FIXED, а приближенного представления – FLOAT. К сожалению, сами названия «FIXED/FLOAT» (вместо «ТОЧНОЕ/ПРИБЛИЖЕННОЕ») не вносят ясности и интуитивно непонятны. Как, кстати, и распространенное в языках, но совершенно неинформативное название «double».

В операторах описания на PL/1 точное представление чисел имеет атрибуты FIXED DECIMAL, после которых указывается общий размер числа и размер дробной части числа, например, FIXED DECIMAL(15,0)или FIXED DECIMAL(12,4) т.е. так можно точно представлять и целые и нецелые значения. Значения можно также представлять и как FIXED BINARY, но далее будет рассматриваться только тип, который может иметь десятичную дробную часть.

В рассматриваемом трансляторе этот тип реализован в двоично-десятичном упакованном коде BCD (Binary Coded Decimal). Каждая цифра BCD записывается в половину байта. При этом старшая значащая пара цифр BCD размещается по старшему адресу. Самая старшая позиция BCD резервируется для знака, который для положительных чисел — ноль, для отрицательных — девять. Число байт для FIXED DECIMAL определяется заданной «точностью» p (допустимой в стандарте языка от 1 до 15) и равно FLOOR((p+2)/2).

Например, число 12345 с точностью 5, для которого PL/1 выделит FLOOR((5+2)/2)=3 байта памяти, будет записано в байтах как 45 23 01. Максимально допустимое значение, которое можно записать в таком виде 999 999 999 999 999. Например, чтобы представить точно значение государственного долга США, который,например, на начало 2013 года составлял около $16,400,000,000,000, потребуется переменная типа FIXED DECIMAL(14,0). Правда, если долг вырастет еще раз в 60, представить его точно в формате стандарта PL/1 будет уже нельзя.

Отрицательное число записывается в дополнительном до 9 коде, который получается вычитанием каждой цифры из 9 и прибавлением к числу единицы. Например, число -2 имеет код (9-2)+1=8 и с точностью 1 будет записано как байт 98, а с точностью 5 будет выглядеть в байтах как 98 99 99.

Может показаться странным, что в этом представлении нет указания положения десятичной точки. Но ведь и в логарифмической линейке не было указателя точки – приходилось держать ее позицию в уме. В данном случае за положением десятичной точки следит транслятор и явно указывает ее как параметр при вызове подпрограмм преобразований, ввода-вывода и т.п. А при генерации самих вычислений положение точки рассчитывается транслятором так, чтобы не терялась точность, и тоже «держится в уме» пока ее не потребуется использовать. Если вернуться к примеру, при генерации умножения транслятор назначит константе 1.3 атрибуты FIXED DECIMAL(2,1), константе 0.13 — атрибуты FIXED DECIMAL(3,2), а результату (значению 0.169) атрибуты FIXED DECIMAL(6,3), совсем как при умножении «столбиком», когда «ширина» результата складывается из размеров множителей. Правда в данном случае «ширина» должна была бы быть 5, а не 6, но по особенности PL/1 он считает длину результата при точном умножении как p1+p2+1, поскольку стандарт учитывает даже случай умножения комплексных чисел по формуле (a1a2-b1b2)+(a1b2+b1a2)i, где становится возможен еще один перенос разряда.

Системные вызовы точных вычислений

Поскольку точные вычисления не могут быть реализованы за одну-две команды, транслятор генерирует множество вызовов системных подпрограмм, обеспечивающих все необходимые действия: арифметику, функции FLOOR/CEIL/TRUNC, сравнение и т.п. Операнды типа FIXED DECIMAL передаются через стек и туда же возвращается результат операции. При этом операнды всегда расширяются до максимальной длины 8 байт и дописываются или нулями или байтами 99 (для отрицательных).

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

;══════════════════ ВЫЧИТАНИЕ DECIMAL В СТЕКЕ ═════════════════ PUBLIC ?DSUOP: ;ГЕНЕРИРУЕТСЯ ВЫЗОВ ИЗ PL/1 LEA ESI,[ESP]+4 ;НАЧАЛО 1 DECIMAL (УЧЛИ RET) MOV ECX,8 ;МАКСИМАЛЬНАЯ ДЛИНА DECIMAL CLC ;СБРОС ПЕРЕНОСА LEA EDI,[ESI+ECX] ;НАЧАЛО 2 DECIMAL ;---- ЦИКЛ ВЫЧИТАНИЯ ---- M2187:MOV AL,[EDI] SBB AL,[ESI] ;ВЫЧИТАНИЕ С КОРРЕКЦИЕЙ DAS STOSB ;ЗАПИСЬ РЕЗУЛЬТАТА INC ESI LOOP M2187 POP ECX ;АДРЕС ВОЗВРАТА MOV ESP,ESI ;ОЧИСТКА СТЕКА JMP ECX ;══════════════════ СЛОЖЕНИЕ DECIMAL В СТЕКЕ ══════════════════ EXTRN ?DOVER:NEAR PUBLIC ?DADOP: ;ГЕНЕРИРУЕТСЯ ВЫЗОВ ИЗ PL/1 LEA ESI,[ESP]+4 ;НАЧАЛО 1 DECIMAL (УЧЕТ RET) MOV ECX,8 ;МАКСИМАЛЬНАЯ ДЛИНА DECIMAL CLC ;СБРОС ПЕРЕНОСА LEA EDI,[ESI+ECX] ;НАЧАЛО 2 DECIMAL ;---- ЦИКЛ СЛОЖЕНИЯ ДВУХ DECIMAL В СТЕКЕ ---- M2283:LODSB ADC AL,[EDI] ;СЛОЖЕНИЕ С КОРРЕКЦИЕЙ DAA STOSB ;ЗАПИСЬ ОТВЕТА LOOP M2283 ;---- ПРОВЕРКА НА ПЕРЕПОЛНЕНИЕ ---- AND AL,0F0H ;ВЫДЕЛИЛИ ПОСЛЕДНЮЮ ЦИФРУ JZ @ CMP AL,90H ;ОТРИЦАТЕЛЬНЫЙ НЕ ПЕРЕПОЛНИЛСЯ ? JNZ ?DOVER ;OVERFLOW ДЛЯ DECIMAL ;---- ВЫХОД С ОЧИСТКОЙ СТЕКА ---- @: POP ECX ;АДРЕС ВОЗВРАТА MOV ESP,ESI ;ОЧИСТКА СТЕКА JMP ECX

Как следует из этого текста, для двоично-десятичной арифметики используются команды сложения и вычитания с учетом переноса (ADC/SBB) и две специфические команды коррекции результата (DAA/DAS), те самые, которые исключены в 64-разрядном режиме.

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

Обратите внимание, что нет принципиальных ограничений на длину данных типа FIXED DECIMAL: цикл побайтной обработки пар цифр можно сделать любой длины (а, значит, и точности), хотя в соответствии со стандартом языка PL/1 под каждый операнд при вычислениях выделяется 8 байт и эта максимальная длина записана как константа. Если в трансляторе изменить предел размера типа, например с 15 до 31, а в системных подпрограммах заменить константу 8 на 16, то переменные автоматически станут получать длину до 16 байт и обрабатываться с точностью до 31 десятичного разряда теми же самыми подпрограммами. Скорость обработки при этом уменьшится.

Выполнение точных вычислений в PL/1

Вернемся к простейшему примеру точных вычислений, теперь на языке PL/1, добавив деление на ту же константу, чтобы вернуть исходное значение переменной. Вычисления проведем и для типа FIXED DECIMAL и для типа FLOAT(53) – аналога типа «double» в других языках.

test:proc main; dcl x fixed decimal(6,3); x=1.3; x=x*0.13; put skip data(x); x=x/0.13; put skip data(x); dcl y float(53); y=1.3; y=y*0.13; put skip data(y); y=y/0.13; put skip data(y); end test;

Результат вычислений представлен на рисунке.

вычисления с точным и приближенным значениями

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

Таким образом, аппарат точных вычислений, встроенный в язык PL/1, достаточно прост, компактен, легко реализуется в системной библиотеке и всегда готов к применению. Но иногда такие возможности играют злую шутку с программистами, ранее имевших дело только с целыми и действительными числами и даже не подозревающими о наличии в языке точных вычислений. Например, если в программе на PL/1 в числовой константе нет показателя степени E и при этом явно не указано на преобразование в тип FLOAT, то по умолчанию считается, что требуется точное представление и точные вычисления. Поэтому, например, выражение 25.0+1/3 даст исключение переполнения при выполнении, поскольку транслятор разместит рациональное число 1/3 с максимальной точностью (т.е. займет все допустимые 15 десятичных разрядов) и потом добавить еще два разряда целой части результата сложения будет уже «некуда». В то же время выражение 25.0+1Е0/3 будет вычислено приближенно и никакого переполнения не произойдет.

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

Заключение

«Хранить деньги», т.е. проводить расчеты, избегая округлений, лучше всего на языках, специально разработанных для решения экономических задач типа Кобола или PL/1 или «Бухгалтерия 1С». Однако как показано выше, реализация средств точных вычислений достаточно проста и практически на любом языке можно самостоятельно создать свой аппарат такой обработки. Например, в книге [4] приводится пример организации вычислений со «сверхточностью» и в «сверхдиапазоне». Процессоры архитектуры IA-32 даже имеют специальные команды, облегчающие арифметические вычисления без округлений. Правда в 64-разрядном режиме используемая для этой цели пара команд DAA/DAS становится недоступной, однако это не сильно затруднит реализацию, поскольку такие команды несложно эмулировать программно, например:

;--------------- ЭМУЛЯЦИЯ DAA, ЗАПРЕЩЕННОЙ В РЕЖИМЕ X86-64 ------------------ PUBLIC DAA_X86_64: PUSH RDX,RAX LAHF MOV EDX,EAX ;OLD CF И OLD AL AND AH,NOT 1B ;СБРОСИЛИ CF ;---- ОБРАБОТКА МЛАДШЕЙ ТЕТРАДЫ ---- TEST AH,10000B ;ЕСЛИ ЕСТЬ AF JNZ @ PUSH RAX AND AL,0FH CMP AL,9 ;ИЛИ ЕСЛИ ЦИФРА БОЛЬШЕ 9 POP RAX JBE M2270 @: ADD AL,6 ;КОРРЕКЦИЯ ЦИФРЫ OR AH,10000B ;УСТАНАВЛИВАЕМ AF ;---- ОБРАБОТКА СТАРШЕЙ ТЕТРАДЫ ---- M2270:TEST DH,1B ;ЕСЛИ СТОЯЛ OLD CF JNZ @ CMP DL,99H ;ИЛИ НУЖЕН ПЕРЕНОС JBE M2271 @: OR AH,1B ;УСТАНАВЛИВАЕМ CF ADD AL,60H ;КОРРЕКЦИЯ ТЕТРАДЫ ;---- ПИШЕМ ГОТОВЫЙ БАЙТ И ВОССТАНАВЛИВАЕМ РЕГИСТРЫ И ФЛАГИ ---- M2271:SAHF MOV [RSP],AL POP RAX,RDX RET

Основой точных вычислений является точное представление чисел в памяти компьютера. Двоично-десятичное представление позволяет легко организовать точные вычисления для чисел любой длины, т.е. любой точности. Проверено на практике, что для большинства экономических расчетов вполне хватает длины в 15 десятичных разрядов.

Литература

1. Д.Р. Джадд «Работа с файлами». Издательство «Мир» Москва, 1975

3. Д. Караваев «К вопросу о совершенствовании языка программирования». RSDN Magazine #4 2011

4. Л.Ф. Штернберг «Ошибки программирования и приемы работы на языке ПЛ/1», Москва «Машиностроение», 1993

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *