跳至内容
Topos Uranos

摘要

程序中带小数的数并不是数学中的实数:它们是分母为 2 的幂的有理数所构成的一个有限子集,凡不属于这个子集的值,都被替换为子集中离它最近的值。本课研究浮点数如何以 64 位(符号、指数和尾数)保存,为什么 0.10.1 没有精确的表示、由哪个值取而代之,float 和 double 中有多少位数字是可靠的,舍入误差如何累积,为什么不能用 == 比较两个实数、又该如何用有依据的容差来比较,以及这种格式为放不下或不存在的结果所保留的无穷大和非数是什么。

学习目标

  1. 解释为什么 0.10.1 没有精确的二进制表示,以及取而代之保存的是哪个值。
  2. 预测舍入在重复求和以及数量级相差悬殊的数相加时的影响。
  3. 用合适的容差而不是 == 比较实数,并说明选择该容差的理由。
  4. 根据所需精度在 float 与 double 之间作出选择,并能识别无穷大和非数。

以 2 为底的科学记数法

整数及其表示一课表明,一个 nn 位的整数恰好表示 2n2^n 个连续的值。然而,带小数的数提出了另一种困难:任意两个实数之间都有无穷多个其他实数,任何有限的位数都无法把它们全部区分开。当今所有计算机采用的办法,与科学书写极大或极小的量时所用的办法相同:科学记数法。以 10 为底时,地球到太阳的平均距离写作 1.496×10111.496 \times 10^{11} 米;这个数有一个符号、少数几位有效数字(尾数,1.4961.496)和底数的一个幂(指数,1111),指数说明小数点的位置。既然如此,用固定位数的数字就能覆盖数量级相差悬殊的量,代价是每个量只保存其前几位数字。

计算机以 2 为底做同样的事。每个非零的数都能以唯一的方式写成

x=(−1)s×1.m1m2m3…m52 (2)×2ex = (-1)^{s} \times 1.m_1 m_2 m_3 \ldots m_{52} \, {}_{(2)} \times 2^{e}

其中 ss 是符号(正数为 0,负数为 1),1.m1m2…1.m_1 m_2 \ldots 是用二进制写出的尾数,其首位总是 1,ee 是指数。首位总是 1 有一个实际的好处:不必保存它,省下的这一位用于提高精度。浮点这个名称描述的正是这种形式:小数点在数字之间没有固定的位置,而是随指数“浮动”。

C++ 的 double 类型在所有现行编译器中都采用国际标准化的 64 位双精度二进制格式:1 位符号,11 位指数,52 位尾数(不含首位数字)。指数以偏移的方式保存:11 位的字段存放 e+1023e + 1023,因此对于从 −1022-1022 到 10231023 的指数,它保存从 1 到 2046 的无符号值(字段的值 0 和 2047 保留给零、非常小的数、无穷大和非数,本课末尾将会讲到)。float 类型采用 32 位的单精度格式:1 位符号,8 位指数,23 位尾数。

double 的各个位是可以看到的。下面的程序使用 std::bit_cast(来自头文件 <bit>),它把这个数的 64 位重新解释为一个 64 位的无符号整数,一位也不改变;这里不作进一步解释,只把它当作观察内存的工具。格式说明 {:064b} 用 64 位数字以二进制写出这个整数,如同前一课那样,{:016X} 则用 16 位数字以十六进制写出。

C++ 程序
#include <bit>#include <cstdint>#include <print>​int main(){    const double tenth{0.1};    // 这个数的 64 位,作为无符号整数读取。    const std::uint64_t bits{std::bit_cast<std::uint64_t>(tenth)};    std::println("{:064b}", bits);    std::println("{:016X}", bits);}
输出
00111111101110011001100110011001100110011001100110011001100110103FB999999999999A
例题0.1 的 64 位

图示把程序写出的 64 位分组,逐个字段地解读。

证明

  1. 1+11+52=641 + 11 + 52 = 64

    这 64 位用十六进制表示为 3FB999999999999A。从左到右读:1 位符号,11 位指数,52 位尾数。

  2. s=0s = 0

    符号为 0:这个数是正数。

  3. e=1019−1023=−4e = 1019 - 1023 = -4

    指数字段为二进制的 0111111101101111111011,值为 1019;减去偏移量 1023,指数为 −4-4。

  4. m≈1.6000000000000000888m \approx 1.6000000000000000888

    尾数连同不保存的首位 1,用二进制表示为 1.1001100110011…10101.1001100110011\ldots1010:其中 10011001 这一组不断重复,最后一组 10101010 是向上舍入的结果。它的值约为 1.60000000000000008881.6000000000000000888。

  5. x=0.1000000000000000055511…≠0.1x = 0.1000000000000000055511\ldots \neq 0.1

    保存的数是 +1.6000000000000000888×2−4+1.6000000000000000888 \times 2^{-4},即 0.1000000000000000055511…0.1000000000000000055511\ldots:非常接近 0.10.1,但不是 0.10.1。