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

Введение.

Когда-то давно, ещё в прошлом веке, програмирование было сродни тайному знанию. В СССР строили вычислительные центры, где стояли купленные за народное золото IBM PC и наши БЭВМ. К этим учреждениям в полной мере подходила фраза из кинофильма "Чародеи":

- Какие люди здесь наверное работают! Какие вопросы решают?!
- Кто поставил сюда этот вопрос?
- А он вместе с другими в кладовке лежал...

Вот и "интеллектуальная элита" СССР, работающая в этих НИИ и ВЦ, "решала вопросы"... оцифровывала картину "Мона Лиза". И народ восхищался...

Сейчас дела обстоят не лучше - народ сходит с ума от блокчейна и майнит-майнит-майнит..., но суть не в этом. Тогда программирование было доступно немногим. Не говоря уже о IBM PC, даже отечественные "Агаты" было сложно купить. Правда в продаже были "народные персоналки" - приставки к телевизору, программы на которые загружались с магнитофона. Языком программирования у них был большей частью Бейсик... Помниться, была у меня в те годы такая штука. Правда микросхема знакогенератора, видимо из-за неудачной схемотехники, часто горела, так что дважды приходилось отправлять на завод для ремонта. И ждать по полгода. Хорошо хоть ПМК БЗ-34 работал...

Ладно, с ностальгической частью заканчиваю.

Сейчас програмирование доступно всем. И вот уже школьники что-то там пишут под Android и выкладывают это на Play Market. Часто это простенькие калькуляторы. Только вот считают эти калькуляторы с ошибками. Ладно школьники, но и гораздо более серьёзные "дяди" позволяют себе выкладывать некорректные продукты. Про ошибки симуляции/эмуляции МК-61 программой "Калькулятор 3000" я уже писал ранее.

Отчего чаще всего возникают ошибки в расчётах? Про эмулятор "Калькулятор 3000" я писать не буду - не знаю. Возможно была некорректно восстановлена логика работы микросхем т.е. некорректно проделан реверсинжинеринг. (Ещё здесь и здесь, для понимания термина). Но наиболее вероятно, что "обвязка" высокоуровневым кодом в "Калькулятор 3000" выполнена некорректно. Когда-то встречал сентенцию о мастере и подмастерьи. Так вот, автор "Калькулятор 3000" просто "сапожник" выкинувший своё "детище" в далёком уже 2012 году и более не пожелавший с ним работать - довести до ума. Другие реализации симуляторов/эмуляторов МК-61 созданы корректнее хотя и менее красочны по оформлению интерфейса.

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

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

Создаем в Visual Studio (Delphi, C++ Builder и прочее - не важно) форму. "Накидываем" туда кнопки потом в обработчиках нажатия пишем что-то вроде:

btnPlus.OnClick(){
result = a + b;}
btnMinus.OnClick(){
result = a - b;}

Само-собой, что приведённый код условный, потому как я уже написал - на данном этапе всё равно какой-именно инструмент визуального программирования используется. Важно, что получение результата мы достигаем всего одной строкой вида {result = a + b;}. Дальше начинается чуть интереснее. Если мы пишем на каком-нибудь Python, то можем не определять заранее переменные result, a,b - интерпретатор всё сделает за нас и результат мы получим с той точностью, какая у него заложена по умолчанию (всё чуть сложнее, но не суть важно). В языках чуть более строгих нужно будет определить размерность переменных. Вот тут уже интерснее ведь встроенные в язык типы переменных имеют ограничения. Но ещё интереснее, что размерность этих типов тоже не постоянна. В коротенькой заметке о free pascal я уже приводил пример. Что будет если мы изначально захотим, чтобы наш калькулятор считал с точностью до 19 (девятьнадцатого) знака? А если до 119? То-то и оно. Размерности встроеных типов не хватит и начнутся "танцы с бубном". Это если идти к разработке так сказать "сверху" - от интерфейса. Что бы быть уверенным в том, что наш калькулятор считает действительно правильно (о вылавливании ошибок в том числе логических, пока умолчим) нам, возможно, будет проще не полагаться на стандартные инструменты языка и стороних (для нас) библиотек, а все низкоуровневые процедуры описать самостоятельно.

Начало: техническое задание

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

Давайте попробуем написать софт-калькулятор со следующими требованиями:

  • Требуемая точность 19 знаков.
  • Числа могут быть только положительными.
  • Выполняется два арифметических действия (сложение и вычитание) с точностью см. п. 1

 

По мере развития идеи требования будут расширятся. Пока же достаточно и этого.

Реализация.

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

"Высокий" уровень.

Для того, чтобы не захламлять свой компьютер изрядно разжиревшим софтом вроде Visual Studio, я буду использовать Free Pascal. Ещё раз упомяну - выбор инструмента значения не имеет. Для решения поставленной задачи подойдёт любой инструмент. Точно также не важна сейчас и "тонкая настройка" этого инструмента (оптимизации). Больше ничего не потребуется.

В качестве формальности приведу пару ссылок на учеблики по языку Паскаль: раз, два.

Для начала введём условия:

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

 

 

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

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

program Calc;
//Описание типа числа калькулятора
type num = record
znak : boolean;
_num : array [0..18] of byte;
end;
var
a, //Переменные программы
b,
reslt : num;
n,k : byte; //Вспомогательные переменные для вывода
//результата
{==========================================================================}
{ Процедура элементарного сложения мантисс двух чисел без учёта положения }
{ десятичной точки. }
{==========================================================================}
procedure __add (x, y : num; var res : num);
var
i : byte = 18;
f : boolean = false;
j : byte = 0;
begin
repeat
j := x._num[i] + y._num[i];
if f then
inc(j);
if j > 9 then
begin
res._num[i] := j - 10;
f := true;
end
else
begin
res._num[i] := j;
f := false;
end;
dec(i);
until i = 0;
end;
{===========================================================================}
{ Процедура элементарного вычитания двух целых положительных чисел при усло-}
{ вии, что уменьшаемое больше вычитаемого. }
{===========================================================================}
procedure ___sub(x, y : num; var res : num);
var
i : byte = 18;
f : boolean = false;
j : byte = 0;
begin
repeat
j := x._num[i];
if f then //Если был заём из текущего разряда
dec(j); //уменьшаем значение уменьшаемого
if (j < y._num[i]) then //Если разряд уменьшаемого меньше вычитаемого...
begin
f := true; //Есть заём из старшего разряда
res._num[i] := 10 + j - y._num[i];
end
else //если заёма нет то просто вычитаем
begin
f := false;
res._num[i] := j - y._num[i];
end;
dec(i); //Корректировка счётчика цикла
until i = 0; //Проверка условия завершения цикла
end;
begin
//Инициализация переменных
a._num[18] := 1;
a._num[17] := 2;
a._num[16] := 3;
a._num[15] := 4;
b._num[18] := 4;
b._num[17] := 3;
b._num[16] := 2;
b._num[15] := 1;
//Вычисление суммы
__add(a, b, reslt);
//Вывод значений
Write('a + b = ');
for k := 0 to 18 do
begin
n := reslt._num[k];
Write(n);
end;
WriteLn();
Write('a - b = ');
//Вычисление разности и вывод результата
__sub(a, b, reslt);
for k := 0 to 18 do
begin
n := reslt._num[k];
Write(n);
end;
WriteLn();
end.

Для нашего числа, согласно начальных условий, придумываем структуру потому как ни один встроенный тип не подходит и, соответственно встроенные операции тоже не подходят.
type num = record
znak : boolean;
_num : array [0..18] of byte;
end;

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

Так как для Pascal массив байт это не многобайтное число, то вывод результата требует цикла, как и обработка отдельных разрядов числа проходит в цикле. В остальном реализована элементарная школьная логика поразрядного "вычисления в столбик". При сложении если результат превышает "9", то формируется сигнал о переносе в старщий разряд. При вычитании, соответственно, сигнал формируется, когда разряд вычитаемого больше разряда уменьшаемого. Сами сигналы оформлены как логические флаги. Ничего сложного. Задаём в программе (ввода/вывода пока нет) начальные тестовые значения и запускаем.

Calc img 1

Calc img 2

Итак, простейший тестбенч прошел - полученные значения достоверны. А что будет, если уменьшаемое будет меньше, чем вычитаемое? Меняем значения и... упс, ошибка. Смотрим в отладчике и видим, что ошибку выдаёт встроенная функция Dec(). И хорошо, что её использовали, а то бы голову ещё домали что да как. Процедура вычитания простая и вносить в неё какой-либо код смысла не имеет. Поэтому надо искать другой путь. Какой? В данном случае по условию у нас оба числа положительные. Результат предполагается отрицательный. Этот результат является дополнением к вычитаемому потому как выражение a - b = c можно представить как a - (b + c) = 0. Действительно: 5 - 3 = 2 или 5 - (3 + 2) = 0. Аналогично когда вычитаемое больше уменьшаемого: 3 - 5 = -2 или 3 - (5 +(-2)) = 3- (5-2) = 0. Раскрывая скобки в выражении мы приходим у школьному правилу (второй или третий класс?), что если вычитаемое больше уменьшаемого, то для положительных чисел мы вправе поменять их местами и перед результатом поставить знак отрицательного числа. Упс. Вспоминается, как в первом классе на счётных палочках учили что такое "больше" и "меньше". И ведь встроенную функцию "<", ">", "<=" и прочее ко всему числу не применишь потому как не понимает его Pascal. Да и остальные языки тоже не поймут.

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

function FindMax(x, y : num) : byte;
var
i : byte;
begin
for i := 0 to 18 do
begin
if (x._num[i] = y._num[i]) then
continue;
if (x._num[i] < y._num[i]) and (y._num[i] <> 0) then
begin
FindMax := 1; //Первое число меньше второго
break;
end
else
begin
FindMax := 2; //Первое число больше второго
break;
end;
FindMax := 0; //Все цифры в числе равны
end;
end;

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

procedure __sub(x, y : num; var res : num);
begin
if FindMax(x, y) = 1 then
begin
___sub(y, x, res);
res.znak := true; //Результат отрицательный
end
else
begin
___sub(x, y, res);
res.znak := false; //Результат положительный
end;
end;

Текст вывода результата тоже немного нужно будет поменять:

//Вычисление разности и вывод результата
_sub(a, b, reslt);
if reslt.znak = true then
Write('-');
for k := 0 to 18 do
begin
n := reslt._num[k];
Write(n);
end;
WriteLn();
...

Проверяем...

Calc img 3

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

procedure _sub(x, y : num; var res : num);
var
f : byte;
begin
f := FindMax(x, y);
if x.znak then //Если уменьшаемое отрицательно...
begin
if y.znak then //и вычитаемое отрицательно...
begin
if f = 1 then //
begin
___sub(y, x, res); //Результат положителен
res.znak := false;
//-5-(-7) = 2
end
else //Уменьшаемое больше вычитаемого по модулю
begin //но при этом они оба отрицательны...
___sub(x, y, res);
res.znak := true; //результат отрицателен
//-7-(-5) = -2
end;
end
else // else for y.znak
begin //Уменьшаемое отрицательно, вычитаемое положительно
___add(y, x, res);
res.znak := true; //Знак результата отрицательный
end;
end
else // for x.znak = false : уменьшаемое положительно
begin
if y.znak then //вычитаемое отрицательно...
begin
___add(x, y, res); //Результат положителен
res.znak := false; //Знака "минус" нет - число положительно
end
else //вычитаемое положительно...
begin
__sub(x, y, res);
end;
end;
end;

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

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

Ну а дальше ещё умножение и деление - тоже интересные для реализации группы процедур и функций. А ещё дальше такие "простые" тригонометрические функции, как sin и cos. :) Листинг реализации этих функций потребует уже не страниц формата А4, а рулонов пипифакса :).

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

"Низкий" уровень

Для програмирования на уровне машинозависимого кода будем использовать ассемблер Fasm, отладчик-эмулятор emu8086 (или тут), ну и графическую среду разработки Fresh. Cобственно достаточно будет одного эмулятора-отладчика. Я не буду затрагивать азы программирования на ассемблере для IBM PC. Сам я его изучал уже более пятнадцати лет назад и, честно говоря, уже успел забыть. Но кое что могу рекомендовать:


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

 

Ещё раз напомню, что у меня нет цели написать оптимальный код именно под процессор х86-архитекруты. Точно также я понимаю, что работая только с байтами мы теряем 90% и более от возможностей х86-64-архитекруты, но это опять же не важно. Так что инструментария под 8086 вполне хватит...

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

Заключение

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

В целом объем труда, затраченный хоть на написание собственного кода, хоть на реверсинжинеринг примерно одинаковы. Не мудрено, что на форумах, посвященных калькуляторам, почти всегда найдётся кто-то, кто заявит: мне достаточно Python для решения всех практических задач, а если его не хватит, то есть MadCad и другой специализированный софт. Всё это верно конечно, но иногда инрересно "заглянуть под капот".

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Комментарии   

+2 # Лекс 26.10.2017 11:13
У Вас интересные публикации. Жаль, что ваша RSS лента не обновляется. Многое проходит мимо
Ответить | Ответить с цитатой | Цитировать
0 # Beast 31.10.2017 22:40
Да, очень жаль.
Ответить | Ответить с цитатой | Цитировать