Посмотрим правде в глаза: никто не любит дроби, даже компьютеры.

Когда мы говорим о COBOL, первый вопрос, который у всех возникает всегда: Почему мы все еще используем его во многих критических местах? Банки все еще используют COBOL, около 7% ВВП зависит от COBOL в форма платежей от Центров услуг Medicare и Medicaid, IRS, как известно, по-прежнему использует COBOL, авиакомпании по-прежнему используют COBOL (Адам Флетчер обронил мой любимый забавный факт по этой теме в своей статье Системы, которые мы любим talk : номер бронирования на вашем билете раньше был просто указателем), многие критически важные объекты инфраструктуры как в частном, так и в государственном секторе все еще работают на COBOL.

Почему?

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

Но когда я работал с IRS, старые разработчики COBOL говорили мне: «Мы пытались переписать код на Java, и Java не могла правильно выполнять вычисления».

Мне это показалось очень странным. Настолько, что мне сразу пришла в голову паническая мысль: «Боже мой, IRS собирает все налоговые счета в течение 50 лет !!!» Я просто не мог поверить, что COBOL может превзойти Java в математике, необходимой IRS. В конце концов, они не запускали людей в космос в Нью-Кэрроллтоне.

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

В чем ваша точка зрения?

Я сделал небольшой перерыв в написании статей о COBOL, чтобы написать о способах хранения информации в компьютерах до того, как двоичный код стал стандартом де-факто (а также учебником по использованию интерфейса z / OS, но это ни здесь, ни там). Оказалось, что при рассмотрении этой проблемы это было полезным отступлением. В этом посте я рассказал о различных способах использования состояний включения / выключения для хранения чисел с основанием 2, чисел с основанием 3, чисел с основанием 10, отрицательных чисел и так далее. Единственное, что я упустил, это ... Как мы храним десятичные дроби?

Если вы проектировали свой собственный двоичный компьютер, вы могли бы начать с простого представления с базой 2. Биты слева от точки представляют 1,2,4,8… и биты справа от точки представляют 1/2, 1/4, 1/8…

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

Разницу легко вычислить самостоятельно: расстояние между 1/16 и 1/32 равно 0,03125, а расстояние между 1/2 и 1/4 равно 0,25.

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

Классический пример этого - 0,1 (одна десятая). Как мы представим это в двоичном формате? 2-¹ - это 1/2 или 0,5, что слишком велико. 1/16 - это 0,0625, что слишком мало. 1/16 + 1/32 приближает нас (0,09375), но 1/16 + 1/32 + 1/64 сбивает нас с 0,109375.

Если вы думаете, что это может продолжаться бесконечно: да, это именно то, что он делает.

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

Это отличное решение этой проблемы, за исключением того, что вам нужно исправить десятичную / двоичную точку в определенном месте. В противном случае 10.00001 и 100000.1 станут одним и тем же числом. Но с позициями справа от точки, установленными на два, мы округлим 10.00001 до 10.00, и 100000.1 стало бы 100000.10.

Вуаля! И это фиксированная точка.

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

Повторение Мюллера

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

COBOL был разработан для фиксированной точки по умолчанию, но означает ли это, что COBOL лучше в математике, чем более современные языки? Если мы будем придерживаться таких задач, как .1 + .2, ответ может показаться утвердительным, но это скучно. Давайте продвинемся еще дальше.

Мы собираемся поэкспериментировать с COBOL, используя что-то под названием Muller's Recurrence. Жан-Мишель Мюллер - французский ученый-компьютерщик, у него, пожалуй, лучшая работа в области компьютерных наук в мире. Он находит способы сломать компьютеры, используя математику. Я уверен, что он сказал бы, что изучает проблемы надежности и точности, но нет-нет-нет: Он разрабатывает математические задачи, которые ломают компьютеры. Одна из таких проблем - его формула повторения. Это выглядит примерно так:

Это не так страшно, правда? Проблема повторения полезна для наших целей, потому что:

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

А вот быстрый скрипт на Python, который параллельно создает версии Muller's Recurrence с плавающей запятой и с фиксированной запятой:

from decimal import Decimal
def rec(y, z):
 return 108 - ((815-1500/z)/y)
 
def floatpt(N):
 x = [4, 4.25]
 for i in range(2, N+1):
  x.append(rec(x[i-1], x[i-2]))
 return x
 
def fixedpt(N):
 x = [Decimal(4), Decimal(17)/Decimal(4)]
 for i in range(2, N+1):
  x.append(rec(x[i-1], x[i-2]))
 return x
N = 20 
flt = floatpt(N)
fxd = fixedpt(N)
for i in range(N):
 print str(i) + ' | '+str(flt[i])+' | '+str(fxd[i])

Это дает нам следующий результат:

i  | floating pt    | fixed pt
-- | -------------- | ---------------------------
0  | 4              | 4
1  | 4.25           | 4.25
2  | 4.47058823529  | 4.4705882352941176470588235
3  | 4.64473684211  | 4.6447368421052631578947362
4  | 4.77053824363  | 4.7705382436260623229461618
5  | 4.85570071257  | 4.8557007125890736342039857
6  | 4.91084749866  | 4.9108474990827932004342938
7  | 4.94553739553  | 4.9455374041239167246519529
8  | 4.96696240804  | 4.9669625817627005962571288
9  | 4.98004220429  | 4.9800457013556311118526582
10 | 4.9879092328   | 4.9879794484783912679439415
11 | 4.99136264131  | 4.9927702880620482067468253
12 | 4.96745509555  | 4.9956558915062356478184985
13 | 4.42969049831  | 4.9973912683733697540253088
14 | -7.81723657846 | 4.9984339437852482376781601
15 | 168.939167671  | 4.9990600687785413938424188
16 | 102.039963152  | 4.9994358732880376990501184
17 | 100.099947516  | 4.9996602467866575821700634
18 | 100.004992041  | 4.9997713526716167817979714
19 | 100.000249579  | 4.9993671517118171375788238

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

По крайней мере, вы думаете, что маловероятно, что кто-то будет выполнять рекурсивные вычисления столько раз. Именно это и произошло в 1991 году, когда система управления ракетой« Патриот неправильно рассчитала время и убила 28 человек». И оказывается, что математика с плавающей запятой совершенно случайно взорвала множество вещей. Марк Штадтерр сделал невероятный доклад об этом под названием Высокопроизводительные вычисления: мы просто быстрее получаем неправильные ответы? Вы должны прочитать его, если хотите больше примеров и более подробную историю проблемы, чем я могу здесь предложить.

Проблема в том, что компьютеры не имеют бесконечной памяти и поэтому не могут хранить бесконечное количество десятичных (или двоичных) разрядов. Фиксированная точка может быть более точной, чем плавающая, если вы уверены, что вам вряд ли понадобится больше мест, чем вы отложили. Если вы перейдете, число будет округлено. Ни фиксированная, ни плавающая точка не защищены от повторения Мюллера. Оба в конечном итоге дадут неправильный ответ. Возникает вопрос: когда? Если вы увеличите количество итераций в скрипте Python с 20 до 22, например, окончательное число, полученное с помощью фиксированной точки, будет 0.728107. Итерация 23? -501.7081261. Итерация 24? 105.8598187.

На разных языках эта проблема решается по-разному. Некоторые, например COBOL, позволяют типам данных иметь только определенное количество мест. Python, с другой стороны, имеет значения по умолчанию, которые можно настроить по мере необходимости, если на самом компьютере достаточно памяти. Если мы добавим в нашу программу строку (getcontext().prec = 60), сообщающую десятичному модулю python использовать 60 знаков после точки вместо установленного по умолчанию 28, программа сможет без ошибок выполнить 40 итераций повторения Muller.

Давайте посмотрим, как COBOL справится с той же задачей. Вот COBOL версия Мюллера

IDENTIFICATION DIVISION.
PROGRAM-ID.  muller.
AUTHOR.  Marianne Bellotti.
DATA DIVISION.
WORKING-STORAGE SECTION.
01  X1           PIC 9(3)V9(15)    VALUE 4.25.
01  X2           PIC 9(3)V9(15)    VALUE 4.
01  N            PIC 9(2)          VALUE 20.
01  Y            PIC 9(3)V9(15)    VALUE ZEROS.
01  I            PIC 9(2)          VALUES ZEROS.
 
PROCEDURE DIVISION.
 PERFORM N TIMES
  ADD 1 TO I
  DIVIDE X2 INTO 1500 GIVING Y
  SUBTRACT Y FROM 815 GIVING Y
  DIVIDE X1 INTO Y
  MOVE X1 TO X2
  SUBTRACT Y FROM 108 GIVING X1
  DISPLAY I'|'X1
 END-PERFORM.
 STOP RUN.

Если вы впервые видели программу на COBOL, давайте рассмотрим несколько моментов. Во-первых, это COBOL «свободной формы», который был представлен в 2002 году, чтобы привести COBOL в большее соответствие со структурой современных языков. Традиционно COBOL имеет фиксированную ширину, при этом определенные элементы помещаются в определенные столбцы. Идея думать об исходном коде как о строках и столбцах может показаться странной, но она была предназначена для имитации форматирования перфокарт, как в то время писались программы. Перфокарты состоят из 80 столбцов в поперечнике с определенными столбцами для определенных значений… так же, как и традиционный COBOL.

Главное, что, наверное, бросается в глаза, - это то, как объявлены переменные:

01  X2           PIC 9(3)V9(15)    VALUE 4.

Код 01 в начале строки называется номером уровня, он сообщает COBOL, что мы объявляем новую переменную. COBOL позволит нам связывать или группировать переменные вместе (классический пример - адрес, который может иметь улицу, город и страну в качестве отдельных переменных), поэтому в этом случае номер уровня становится важным.

X2 - это имя переменной, довольно прямолинейно. В конце у нас есть то, что наша переменная установлена ​​в начале программы: VALUE 4. Нет, точка - это не опечатка, именно так COBOL заканчивает строки.

Остается деталь посередине PIC 9(3)V9(35)

PIC можно рассматривать как тип данных char. Он принимает буквенно-цифровые данные. Он даже принимает десятичные дроби. COBOL имеет строгую статическую типизацию с той оговоркой, что большинство его типов гораздо более гибкие, чем другие языки. Вы также должны определить, сколько символов будут занимать переменные при их объявлении, это числа в скобках. PIC 9(3) означает, что эта переменная содержит три символа, которые являются цифрами (представлены 9).

9(3)V9(15) следует читать как 3 цифры, за которыми следует десятичная точка (V), за которой следует еще 15 цифр.

Что дает следующее:

01|004.470588235294118
02|004.644736842105272
03|004.770538243626253
04|004.855700712593068
05|004.910847499165008
06|004.945537405797454
07|004.966962615594416
08|004.980046382396752
09|004.987993122733704
10|004.993044417666328
11|005.001145954388894
12|005.107165361144283
13|007.147823677868234
14|035.069409660592417
15|090.744337001124836
16|099.490073035205414
17|099.974374743980031
18|099.998718461941870
19|099.999935923870551
20|099.999996796239314

Это 15 мест после точки. Если мы изменим X1, X2 и Y на PIC9(3)V9(25), мы добьемся большего:

01|004.4705882352941176470588236
02|004.6447368421052631578947385
03|004.7705382436260623229462114
04|004.8557007125890736342050246
05|004.9108474990827932004556769
06|004.9455374041239167250872200
07|004.9669625817627006050563544
08|004.9800457013556312889833307
09|004.9879794484783948244551363
10|004.9927702880621195047924520
11|004.9956558915076636302013455
12|004.9973912684019537143684268
13|004.9984339443572195941803341
14|004.9990600802214771851068183
15|004.9994361021888778909361376
16|004.9996648253090127504521620
17|004.9998629291504492286728625
18|005.0011987392925953357360627
19|005.0263326115282889612747162
20|005.5253038494467588243232985

Разные мэйнфреймы будут предлагать разные верхние пределы для типа PIC COBOL. IBM выводит всего 18 цифр (по крайней мере, в моей версии). MicroFocus поднимется до 38 цифр.

Сколько стоит точность?

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

Но есть загвоздка ... Python (и в этом отношении Java) не имеют встроенной фиксированной точки, а COBOL имеет.

Чтобы заставить Python выполнять фиксированную точку, мне нужно было импортировать модуль Decimal. Если вы когда-либо работали над проектом с целым набором импортированных файлов, вы уже знаете, что они не бесплатны. На таком языке, как Java (на который люди обычно хотят перейти, когда говорят об избавлении от COBOL), затраты на соответствующую библиотеку могут быть заметно выше. На самом деле вопрос в том, имеет ли смысл беспокоиться об этом для вашего варианта использования. Для большинства программистов беспокойство по поводу снижения производительности при импорте - это крайний вариант преждевременной оптимизации.

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

COBOL - это скомпилированный язык с распределением стека, указателями, объединениями без затрат времени выполнения на преобразование между типами и без диспетчеризации во время выполнения или вывода типов. Java, с другой стороны, работает на виртуальной машине, имеет распределение в куче, не имеет объединений, вызывает накладные расходы при преобразовании между типами и использует динамическую диспетчеризацию и определение типов во время выполнения. Хотя можно минимизировать количество используемых этих функций, обычно это достигается за счет наличия кода, не очень «похожего на Java». Часто переводчики жалуются на то, что результирующий код Java нечитаем и не обслуживается, что в значительной степени сводит на нет цель миграции.

Производительность Java-кода, переведенного с COBOL, UniqueSoft

По крайней мере, вы отклоните эту проблему со словами: «О да, но это Java и Java тоже отстой», помните следующее: большинство современных языков программирования не имеют встроенной поддержки фиксированной запятой или десятичной дроби. (На самом деле я думаю, что НИКТО из них не делает правильного утверждения, но я не мог с уверенностью это проверить). Конечно, вы можете выбрать другой язык с другой комбинацией компромиссов, но если вам нужна точность фиксированной точки, и вы подумайте, что крошечные затраты производительности на импорт библиотеки для этого могут накапливаться, у вас действительно есть только один вариант, и это COBOL.

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

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