如何对二进制数做舍入
我们都很熟悉十进制下的四舍五入。二进制的舍入与之类似,但仍然容易让人困惑,其中最大的难点是小数。比如,小数 0.11101 舍入到小数点后 2 位后为什么会得到整数 1,一眼看上去并不显然。本文讲解二进制小数舍入的一般规则,并解释这些规则的来源。同样的规则也适用于二进制整数的舍入。
IEEE-754 标准定义了五种舍入方法,其中大多数都相当直白。前两种舍入到最接近的值(平局时取偶、平局时远离零);其余的称为定向舍入:向零舍入、向正无穷舍入和向负无穷舍入。用十进制举例最容易理解这些规则:

定向舍入相当直白。本文将详细考察 「舍入到最接近的值;平局时取偶」 这条规则。它是二进制浮点数的默认模式,也是十进制推荐的默认模式。而且它通常不像定向舍入那样容易理解。
将二进制小数舍入到第 n 位的一般规则是:检查第 n 位之后的那一位数字。如果它是 0,则总是向下舍入。如果它是 1,并且其后还有任何一位也是 1,则向上舍入。但如果其后各位全是 0,就必须启用平局判定规则,而通常采用的是 「平局时取偶」。这条规则要求舍入到第 n 位上为 0 的那个数。
为了看清这些规则如何生效,我们把几个数舍入到小数点后 2 位:
- 0.11001 — 向下舍入为 0.11,因为第 3 位上的数字是 0
- 0.11101 — 向上舍入为 1.00,因为第 3 位上的数字是 1 且其后还有数字 1(第 5 位)
- 0.11100 — 启用「平局时取偶」判定规则并向上舍入,因为第 3 位上的数字是 1 而其后各位全是 0。
如果你和我一样,那么这些规则从何而来,乍看之下恐怕并不清楚。我读过的关于舍入的文章,似乎没有哪一篇借助可视化表示把这些规则讲得足够透彻——而我认为,这恰恰是清晰解释所必需的。
舍入的两个候选值
舍入一个数时,我们必须在两个可能的候选值中选一个:要么向下舍入到比原数小的最接近的数,要么向上舍入到比原数大的最接近的数。这两个数的小数位数都不应多于舍入后的结果。
为了演示如何确定这两个数,我们把十进制数 0.42385 舍入到 2 位。它可以被舍入成哪些数?把它和两位小数一起放到数轴上,就一目了然了:

这张图显示,我们的原数落在 0.42 和 0.43 之间。由于 0.42385 = 0.42 + 0.00385,而较小的数是 0.42,可以得出:要找较小的数,只需把第二位之后的小数部分截断即可。而要找较大的数,可以在已经确定的较小的数上增加一个 ULP,也就是给最后一位的数字加 1。或者换个角度看,这意味着我们应当给较小的数加上 0.01 —— 即 1 乘以 10 的负 2 次方,因为我们舍入到 2 位。如果舍入到 3 位,我们就取 3 位对应的较小的数,再加上 1 乘以 10 的负 3 次方。
我们可以把这条确定舍入候选值的规则一般化:
把一个小数舍入到 n 位时,先丢弃第 n 位之后的小数部分得到较小的数,再用它加上一个 ULP —— 1 乘以进制基数的 n 次方 —— 得到较大的数。
现在来求二进制数 0.11011 的舍入候选值。同样舍入到 2 位。按照十进制小数的同一套做法,较小的数是 0.11,因为 0.11011 = 0.11 + 0.00011。然后加上 0.01 得到较大的数,也就是 1 乘以 2 的负 2 次方——因为我们用的是二进制,基数为 2,且舍入到 2 位。这个计算的结果是 0.11+0.01=1.00。
寻找最短距离
「舍入到最接近的值」 这条规则说,我们应当舍入到与原数差值最小的那个数。在数轴上,这意味着原数与两个候选值之间的距离最小。因此,如果把原数和两个候选值放到数轴上,并把距离标记为 x,我们就需要找出哪个 x 更小:

用减法来做非常简单:
这里可以清楚地看到 x1 小于 x2,因此我们应当舍入到 0.11。但如果要舍入的是一个无限二进制小数呢?那样一来,减法就不再是一个简单的操作了。事实证明,还有一种容易得多的办法。
与中点比较
另一种更简便的办法,是判断原数比两个舍入候选值的中点大还是小。来看这张图:

不难得出结论:如果一个数大于中点,它就更接近较大的那个舍入结果,否则就更接近较小的那个。
那么,怎么求这两个候选值之间的中点呢?我们知道 0.11 与 1.00 之间的距离是 0.01。所以只需求出 0.01 的一半,再把它加到较小的数上。一半是 0.001,加到较小的数上得到 0.111 = 0.11 + 0.001。本质上,这等价于直接在较小的数末尾追加一个 1。 下面这张图把相关的数都标在了数轴上:

现在剩下的只有把原数与中点作比较:
按位做字典序比较可以清楚看出,0.11011 小于中点。所以我们应当向下舍入到 0.11——与用距离计算得到的结果一致。
平局时取偶
有时原数到两个舍入候选值的距离恰好相等,此时 「舍入到最接近的值」 这条规则就无法适用了。假设我们要把 0.11011 舍入到小数点后 4 位。我们知道需要求出两个舍入候选值以及用于比较的中点。先求较小的数:把第 4 位之后剩下的数字截断,得到 0.1101。接着在较小的数末尾追加 1 求出中点,得到 0.11011。最后,给较小的数加上 1 个 ULP 求出较大的数 —— 0.1101+0.0001 = 0.1110。
于是我们得到:
- 原数 —— 0.11011
- 向下舍入的目标 —— 0.1101
- 向上舍入的目标 —— 0.1110
- 两个候选值之间的中点 —— 0.11011。
逐位比较可以看到,原数与中点相等,这说明原数正好落在两个舍入候选值的正中间。这正是需要启用规则第二部分「平局时取偶」的情形。二进制数以 0 结尾即为偶数,因此在我们这两个候选值中,较大的那个才是偶数,规则要求向上舍入到 0.1110。
推导舍入的一般规则
我们真的需要比较所有位上的数字吗?正如你可能已经从文章开头列出的规则中猜到的那样——并不需要。我在上一段已经说明,中点数总是在第 n+1 位上以 1 结尾,其中 n 是舍入到的位数。原数与中点在第 n+1 位之前总是相等——这意味着比较应当从这一位开始。
所以,文章开头给出的舍入一般规则,正是来自把原数与两个舍入候选值的中点作比较。这里再把这些规则重复一遍:
舍入到第 n 位的一般规则是:检查第 n 位之后的那一位数字。如果它是 0,则总是向下舍入。如果它是 1,并且其后还有任何一位也是 1,则向上舍入。但如果其后各位全是 0,就必须启用平局判定规则,而通常采用的是「平局时取偶」。这条规则要求舍入到第 n 位上为 0 的那个数。