人质COBOL和数学。 第一部分

让我们面对现实:没有人喜欢分数,即使是计算机。

在谈到COBOL编程语言时,出现在每个人脑海中的第一个问题总是像这样:“为什么人类仍然在许多重要领域使用这种语言?” 银行仍在使用COBOL。 美国约7%的GDP依赖于COBOL处理CMS的付款。 众所周知,美国国税局(IRS)仍然使用COBOL。 这种语言也用于航空领域( 从这里我学到了一个有趣的话题:机票的预订号码曾经是通常的指针)。 可以说,许多非常严肃的组织,无论是私营部门还是公共部门,仍在使用COBOL。



第二部分

该材料的作者(我们今天出版的翻译的第一部分)将找到一个答案,即为什么1959年出现的COBOL语言仍然如此广泛。

为什么COBOL还活着?


对这个问题的传统答案是极度愤世嫉俗的。 组织是懒惰,无能和愚蠢。 他们追求廉价,不愿意投资在现代产品上重写其软件系统。 通常,可以假设如此大量的组织的工作依赖于COBOL的原因是惯性和短视的结合。 在这方面,当然有一些道理。 重写大量混乱的代码是一项艰巨的任务。 很贵 这很复杂。 而且,如果现有软件看起来运行良好,则组织将没有特别强烈的动机去投资进行软件更新的项目。

这一切都是如此。 但是当我在IRS工作时,COBOL的资深人士谈到了他们如何尝试用Java重写代码,结果证明Java无法正确执行计算。

这对我来说听起来很奇怪。 真是奇怪,我立即被危言耸听的人想到:“上帝,这意味着国税局已经将向所有人缴纳的税款四舍五入了! 我简直不敢相信COBOL能够根据IRS所需的数学计算绕过Java。 最后,他们没有将人们送入太空。

在夏天学习COBOL的有趣的副作用之一是,我开始了解以下内容。 关键不是Java无法正确执行数学计算。 关键是Java如何使计算正确。 并且,当您了解了如何用Java执行计算以及如何在COBOL中完成相同的事情时,您将开始理解为什么许多组织发现很难摆脱其计算机遗留物。

什么“ i”应加点?


我将略过COBOL的故事,讨论在数据的二进制表示成为事实上的标准之前计算机是如何存储信息的(但是有关如何使用z / OS 接口的资料 ;这很特别)。 我认为,在审议我们的问题时,朝着这个方向偏离主要主题将是有益的。 在上述材料中,我讨论了使用二进制开关在二进制,三进制,十进制系统中存储数字,存储负数等各种方法。 我唯一没有引起足够重视的是十进制数字的存储方式。

如果您设计了自己的二进制计算机,则可以从决定使用二进制数字系统开始。 该点左侧的位表示整数-1,2,4,8。而右侧的位-小数-1 / 2、1 / 4、1 / 8 ...


2.75二进制表示

这里的问题是要了解如何存储小数点本身(实际上-我应该说“二进制点”-因为毕竟我们在谈论二进制数)。 这不是某种“计算机炼金术”,因此您可以猜测我在说的浮点数和定点数。 在浮点数中,二进制点可以放置在任何位置(也就是说,它可以“浮动”)。 点的位置存储为指数。 与没有机会的情况相比,移动点的能力使得可以存储更大范围的数字。 小数点可以移到数字的最后面,并选择所有位来存储代表很大数字的整数值。 该点可以移到数字的前面并表示非常小的值。 但是,这种自由是以准确性为代价的。 让我们再来看上一个示例中的2.75的二进制表示形式。 从四到八的过渡远远超过从四分之一到八分之一的过渡。 如果我们重新编写如下所示的示例,对于我们来说,想象起来可能会更容易。


我通过眼睛选择了数字之间的距离-只是为了展示我的想法

数字之间的差异很容易自行计算。 例如,1/16和1/32之间的距离是0.03125,但是1/2和1/4之间的距离已经是0.25。

为什么这很重要? 对于整数的二进制表示,这无关紧要-通过用适当的位组合填充二进制记录的相邻数字之间的距离,可以很容易地进行补偿,而不会损失准确性。 但是,在表示小数的情况下,并不是那么简单。 如果您试图“填充”相邻数字之间的“孔”,则某些东西可能会“掉进”(并实际上掉进)这些孔中。 这导致以下事实:二进制格式不可能获得分数的精确表示。

经典数字示例0.1(十分之一)对此进行了说明。 如何用二进制格式表示此数字? 2 -1是1/2或0.5。 太多了 1/16是0.0635。 这太少了。 1/16 + 1/32已经接近(0.09375),但是1/16 + 1/32 + 1/64已经超出我们的需求(0.109375)。

如果您认为这种推理可以无限期地继续进行-那么您是对的-正确的做法

在这里您可以对自己说:“为什么我们不像存储数字1那样仅仅保存0.1? 我们可以毫无问题地保存数字1-因此,我们只需删除小数点并以与存储整数相同的方式存储任何数字。”

这是解决此问题的绝佳方法,除了它需要将二进制/小数点固定在某个预定位置。 否则,数字10.00001和100000.1看起来将完全相同。 但是,如果该点是固定的,例如,将2个数字分配给数字的小数部分,那么我们可以将10.00001舍入为10.00,而100000.1将变为100000.10。

我们只是“发明”了定点数。

通过使用定点数表示不同的值,我们就知道了。 这很容易做到。 使用定点数是否可以促进其他一些问题的解决? 让我们在这里回想起我们的好朋友-关于二进制十进制数字(二进制​​编码的十进制,BCD)。 顺便提一下,这些数字已在大多数科学和图形计算器中使用。 从这些设备(很明显),他们希望得到正确的计算结果。


TI-84 Plus计算器

Muller和Python的复发率


定点数字被认为更准确,这是因为数字之间的“空洞”是恒定的,并且因为仅当您需要想象一个没有足够空间的数字时才进行舍入。 但是当使用浮点数时,我们可以使用相同的内存量来表示非常大和非常小的数字。 没错,在他们的帮助下,不可能准确地表示可访问范围内的所有数字,我们被迫采用四舍五入的方式来填补“漏洞”。

COBOL被创建为默认情况下使用定点数字的语言。 但这是否意味着COBOL在执行数学计算方面比现代语言更好? 如果我们遇到诸如计算值0.1 + 0.2的结果之类的问题,那么似乎前面的问题应该回答为“是”。 但这会很无聊。 因此,让我们继续前进。

我们将使用所谓的穆勒递归关系对COBOL进行试验。 让·米歇尔·穆勒(Jean-Michel Muller)是法国科学家,他可能在信息技术领域取得重大科学发现。 他找到了一种使用数学方法打破计算机正确运行的方法。 我相信他会说他研究可靠性和准确性问题,但是没有,也没有,因为他创造了数学问题,使计算机“崩溃”。 这些任务之一是其复发公式。 看起来像这样:


此示例取自此处。

该公式似乎一点也不可怕。 对不对 出于以下原因,此任务适合我们的目的:

  • 此处仅使用简单的数学规则-无需复杂的公式或深刻的思想。
  • 我们从小数点后两位数字开始。 结果,很容易想象我们正在使用代表一定数量资金的价值。
  • 由计算得出的误差不是很小的舍入误差。 这与正确结果相差整个数量级。

这是一个小的Python脚本,它使用浮点数和定点数计算Mueller递归关系的结果:

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人死亡。 事实证明,浮点计算意外造成了很大的伤害。 这里有一些很棒的东西 ,也许高性能计算只是获得错误答案的一种更快的方法。 如果您想获得有关此处讨论的问题的更多信息并阅读更多示例,请阅读本工作。

问题在于计算机拥有的RAM数量不是无限的。 因此,不可能存储无限数量的小数(或二进制)位置。 如果可以确定定点计算比浮点计算更准确,则可以确信,在点之后需要的数字比使用的格式要少的多。 如果该数字不适合此格式,则将四舍五入。 应该注意的是,定点计算和浮点计算都不受Mueller的递归关系证明的问题的影响。 结果和其他结果都给出错误的结果。 问题是何时发生这种情况。 如果将Python脚本中的迭代次数(例如,从20增加到22),则在计算中获得的具有固定点的最终数目将为0.728107。 23次迭代? -501.7081261。 24? 105.8598187。

用不同的语言,此问题以不同的方式表现出来。 某些字符(例如COBOL)允许您使用参数设置紧密的数字。 例如,在Python中,如果计算机具有足够的内存,则可以配置默认值。 如果我们在程序中添加getcontext().prec = 60行,告诉Python十进制模块,它将在句点之后使用60个位置,而不是默认情况下的28个位置,默认情况下,该程序将能够执行40次迭代的重复关系而不会出错穆勒。

待续...

亲爱的读者们! 您是否因浮点计算的性质而遇到严重的问题?

Source: https://habr.com/ru/post/zh-CN467251/


All Articles