财新传媒 财新传媒

阅读:0
听报道

计算机对实数的理解,就像狗对π的理解一样肤浅。 

—— DeepSeek

撰文 | 姜洋(中国科学院理论物理研究所 2022级博士研究生)

飞来横祸

1991年2月25日,第一次海湾战争正如火如荼地进行着。当天晚上20时40分左右,突如其来的爆炸声划破了沙特达摩地区宁静的夜空——伊拉克发射的"飞毛腿"导弹精确命中美军位于达兰的空军基地。霎时间,原本秩序井然的军营化作一片火海,哀嚎遍野。

美军在化作废墟的军营中搜寻生还者

当时美军为了防范伊拉克的导弹袭击,早已在重要军事据点部署了以"爱国者"导弹为核心的拦截系统。不过在这场致命的袭击中,号称先进的"爱国者"系统忠实地充当了“吃瓜”群众的角色,对来袭的导弹毫无反应。最终,袭击导致28名美军士兵丧生,97人受伤,这成为海湾战争期间美军最惨重的单次伤亡事件。事后的调查显示,这场灾难的罪魁祸首是一个看似微不足道的数字计时器精度问题。

浮点运算的“迷惑”

实数在计算机中的表示,也就是浮点数,是科学计算操作的基本对象。可以想见,只具备有限存储空间的计算机永远也不能精确地表示不可数的实数集合。就像上文提到的爱国者,它的计时器只有24位精度。在连续运行100小时后,系统已经累计出了的0.34秒的误差。对于以6倍音速飞行的导弹而言,这相当于700米的定位偏差,足以让整个防御系统形同虚设。

浮点数表示的标准化肇始于上世纪80年代产生的IEEE754规范。随着体系架构的演进,近三十年来计算机工业完成了从32位至64位系统的变革。现代计算机系统已经普遍支持64位双精度浮点数,但这绝不意味着使用者可以忽视数值精度背后的复杂性。数值计算的艺术就是与误差共舞的艺术,"舞技"不好的表演者会在意想不到之处付出惨痛的代价……

读者对浮点数运算的性质了解多少呢?猜一猜下面这个表达式的结果:

>>> 0.1 + 0.1 == 0.2
 

试着在计算机上运行一下,返回结果为True,看起来一切正常。然而,很容易构造一个可能让人小吃一惊的案例:

>>> 0.1 + 0.2 == 0.3
 

返回的结果为False。事实上,双精度浮点计算下  的结果是

>>> 0.30000000000000004
 

浮点数的表示原理

人类使用十进制系统的原因很可能是我们拥有十根手指。但是在构造一个机器系统时,只有两种状态的比特位是最容易实现的。所以对于计算机,二进制表示是更自然的方式。在表示数时,十进制的位拥有的若干次幂的权重。类似地,二进制(比特)位带有的若干次幂权重。下面的一个从二进制转换至十进制的案例很好地说明了这一点:

十进制的有限小数并不总是二进制中的有限小数,比如十进制在二进制中表示为(横线表示循环节)。当映照在计算机上时,二进制规格化浮点数以科学计数法的形式被存储:

对于位双精度浮点数,计算机使用位来存储的值,不能被表示的有效位采取就近舍入的规则(就近值的误差相等时向偶数舍入)。举一个例子,会按照以下的方式舍入并存储

1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 [1001...]  * 2^(-4)
--------------------------------------------------------------------------------------
1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010  * 2^(-4)
 

恰好是的两倍,表示只需要将中的加,部分则是完全相同的。计算时,发生的是如下的事:

    1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010  * 2^(-4)
+   1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010  * 2^(-4)
--------------------------------------------------------------------------------
   11.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100  * 2^(-4)
=   1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010  * 2^(-3)
 

等号后的结果恰好是的二进制表示。计算机并不能精确地存储与,但它正确地判断了的结果是!

为什么 

对于,它的二进制科学计数法表示是,舍入后得到

    1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 * 2^(-2)
 

由于与的指数不同,计算两数的和时要先对齐指数,先转化成

0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101  * 2^(-3)
 

然后进行加法和舍入:

    1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010  * 2^(-3)
+   0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101  * 2^(-3)
--------------------------------------------------------------------------------
   10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111  * 2^(-3)
=   1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100  * 2^(-2) 
 

这个结果与先前提到的的位级表示并不相同,所以的结果便不等于。它转化成十进制后是

在此,我们恢复了先前计算产生的结果。

爱国者导弹

“爱国者”导弹系统内置了一个用于计时的钟——每秒进行一次更新。与IEEE规则不同,这个运算是通过位固定小数点寄存器实现的——在不采用科学计数法的情况下直接存储的二进制表示。按理说,与最接近的计算机近似表示是它的过剩值:

0.00011001100110011001100[1100...]
-------------------------------
0.00011001100110011001101
 

可是“爱国者”系统莫名其妙地使用了直接截断的不足近似值:

0.00011001100110011001100
 

这一选择让误差又被放大了倍!计时器的每一次自增造成的误差是

事实上,在2月11日,“爱国者”导弹项目组在分析以色列军方提供的系统日志时已经发现了这个软件错误:当系统连续工作8小时以上时,定位目标就会偏离正常位置的。他们在21日向所有使用“爱国者”的部队发出了消息:在“长时间”启动“爱国者“系统的情况下,射程就会发⽣偏离, 导致追踪⽬标的失败。也许是项目组觉得军方肯定不会让导弹系统的运行时间长到无法成功追踪目标,他们在发出的通告中完全没有解释这个“长时间”究竟是多长时间...

2月26日,也就是事故发生后的第二天,修复后的软件被运抵了达兰空军基地。

结语

从现在的视角来看,"爱国者"导弹的悲剧并不是源于什么深奥的技术谜题,但是简单的bug依然可以让人们付出惨痛的代价。人类认识世界、改造世界的步伐就是以这样一种蜿蜒曲折的方式前进着。浮点运算像是布满暗礁的海域,理解了潮汐规律的人才能安全航行。在数字化浪潮席卷一切的今天,每个0与1的抉择都可能成为命运的转折点。我们无法彻底消除误差,但每一次跌倒后的反思都将让我们在下一次跌倒前走得更稳、更远——只要我们永不停止从错误中学习的脚步。

参考文献

[1] Randal E. Bryant, David R. O’Hallaron. Computer Systems: A Programmer’s Perspective (3rd edition). Pearson. 2015.

[2] ⾦钟河,叶蕾蕾译. 致命Bug,软件缺陷的灾难与启⽰. 人民邮电出版社. 2016.

[3]Wikipedia

本文经授权转载自微信公众号“中国科学院理论物理研究所”,原题目为《Doctor Curious 66:浮点数表示之殇:被背叛的爱国者》。

话题:



0

推荐

返朴

返朴

2857篇文章 2小时前更新

科学新媒体“返朴”,科普中国子品牌,倡导“溯源守拙,问学求新”。

文章