BCD、COBOL、千年虫和昭和 100 年问题

上篇文章 里我们讲到了定点数和浮点数的区别,我们认识到这两个数字在精度对待上的差异。但是这两个类型的数字都是二进制数,而有些数字本身就是很难用二进制表达的。

比如我们有一个十进制数 $0.1234$,要将其转换成二进制。如果我们采用 32 位 IEEE 754,其结果是 $00111101111111001011100100100100$,转换回十进制是 $0.1234000027179718017578125$ 造成了一个 $2.717 \times 10^{-9}$ 的误差。而如果我们定点数,我们先尝试直接转换:

二进制转换

我们至少需要一个 503 位的二进制数才能表达到其开始循环的位数。而且这个在十进制中非常容易表达的数字,在二进制中却变成了非常复杂的循环小数。在一些情况下,我们对于数字在十进制下的精度非常敏感(比如银行),在这种情况下我们应该如何处理数字呢?

BCD (二进码十进数)

对于一个 N 位二进制数,我们很容易知道,其能表达的最大数字量是 $2^N$ 个。十进制数的每一位需要能表达 10 个不同的数字。于是我们有方程 $log_{10}2^N = 1, \lceil N \rceil = 4$,我们可以用 4 个二进制数表达 1 个十进制数(忽略多出来的 6 个数字)。也就是如下表的对应关系:

十进制数 8 4 2 1
0 0 0 0 0
1 0 0 0 1
2 0 0 1 0
3 0 0 1 1
4 0 1 0 0
5 0 1 0 1
6 0 1 1 0
7 0 1 1 1
8 1 0 0 0
9 1 0 0 1
忽略 1 0 1 0
忽略 1 0 1 1
忽略 1 1 0 0
忽略 1 1 0 1
忽略 1 1 1 0
忽略 1 1 1 1

这就是 BCD (二进码十进数) 的核心设计理念。

BCD 在计算机中的实现历史悠久。在 Intel 1971 年制造的第一块 CPU Intel 4004 中,其计算系统就是围绕 BCD 设计的(用于驱动计算器)。在之后的 Intel 8008 甚至 x86 架构中都被保留了下来。

COBOL

如果我们想在高级语言中使用这一特性,COBOL 是一个很常用的选择。COBOL 出现于 1959 年,是世界上最早实施标准化的计算机语言之一。这门语言在当年是大型商业软件的热门语言。COBOL 支持一种数据类型称为 Comp-3 (Computational-3, Packed Decimal, Packed),即是我们上面所说的 BCD 数字。

我们来看下面的 COBOL 程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IDENTIFICATION DIVISION.
PROGRAM-ID. bcd-check.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 A1 PIC 9(1)V9(17) VALUE 0.1.
01 A2 PIC 9(1)V9(17) VALUE 0.2.
01 WR PIC 9(1)V9(30).
PROCEDURE DIVISION.
COMPUTE WR = A1 + A2.
IF 0.3 = WR THEN
DISPLAY "TRUE"
ELSE
DISPLAY "FALSE"
END-IF.
STOP RUN.

这个程序会打印 TRUE,也就是 0.1 + 0.2 精确地等于 0.3,采用 BCD 定点数不会像浮点数一样多出一些二进制误差。高级语言有这样的特性支持,底层的 CPU 有对应的 BCD 指令集直接进行计算,这对于开发来说不能说是不完美。

这使得在开发银行的转账业务之类的时候,可以放心大胆地进行加减了,COBOL 在商业领域大放异彩。

x86_64

然而,事情不是一成不变的。

AMD64

1999 年 AMD 扩展了 x86 指令集,提出了新的 amd64 指令集。此后的 Intel 也采用了兼容的指令集。家用计算机从此迈入 64 位时代。并且随着 Intel 在服务器市场的发力,x86_64 也大量取代了原先的小型机、中型机市场。

然而 amd64 真的是 x86 兼容的指令集吗?并不是。比如 BCD 相关的指令就被移除了。BCD 对于现代计算机架构来说过于复杂。而且随着计算机架构的进一步发展,特别是今天的 CPU 大量依靠 SIMD 指令来提高性能,而 BCD 这种无法很好对齐二进制的数据处理方式给 SIMD 带来极大的困难,要想让 BCD 在 CPU 指令级别借尸还魂几乎不可能。

不过这问题不大,毕竟 COBOL 编译器也有在升级,虽然性能上有一定影响但问题不算太大。

千年虫和昭和 100 年问题

令和

昭和虽然在 1989 年就结束了,但是昭和魂却仍大量活在日本的银行系统中。计算机内部需要一套稳定的纪年方法。在 8/16 位机年代,内存寸土寸金。一般纪年都是通过两位数字来表示,为了方便输出通常用的也是 BCD 整数。比如 1919 年就记成 19。然而随着 2000 年的到来,99 变成 00,直接一夜回到 100 年以前,会造成大量系统故障。这就是所谓「千年虫问题」。日本工程师在设计系统时考虑到千年虫问题会在 2000 年到来,如果使用昭和纪年法,要到昭和 100 年才会遇到类似问题,将千年虫问题延后了 25 年,到 2025 年才会被触发。

2025 年就是 5 年后,然而 COBOL 在今天已经是几乎被淘汰的语言了,BCD 在常用 CPU 的指令集中再也找不到了。也就是说当年编译的程序如果没有源码将无法二进制兼容地移到新机器上,更不要说通过二进制修改替换成 32 位或者 64 位 BCD 数来直接避开问题。就算有源码,懂怎么写 COBOL 的人也快变成了珍惜资源。当年决定要用昭和来避开千年虫的人恐怕也早就退休了。

于是五年后的日本将如何应对昭和 100 年问题,让我们继续看下去。