← SEARCHABLE ARCHIVE
Computer Science · 2026-08-08

IEEE 754 如何存储实数?

理解 binary32、binary64、舍入、NaN 与累加误差

INTERACTIVE READING MODE →

IEEE 754 如何存储实数?

本文以教学为目的。硬件、编译选项和语言实现可能改变细节,重要行为请在自己的环境中确认。

初学者常对 0.1 + 0.2 不完全等于 0.3 感到意外。计算机并不是不会加法,而是必须把无限多个实数放进有限的位模式,因此大多数值只能用邻近值表示。

IEEE 754 不只规定位排列,也定义运算、舍入、无穷大、NaN、渐进式下溢、转换与异常状态。本文会拆解 binary32 和 binary64,再整理比较、累加、金额与序列化时的实用原则。

定点数与浮点数

定点数预先固定小数点位置。若整数 12345 永远代表两位小数,实际值就是 123.45。它适合固定尺度,但同一格式难以同时覆盖极小与极大的数。

浮点数类似科学记数法。

6.25 = 1.5625 × 2²
0.15625 = 1.25 × 2⁻³

有效数字和指数分开存储,范围因此很宽,但相邻值之间的距离并不固定。

IEEE 754 涵盖的内容

IEEE 754-2019 规范二进制与十进制格式、运算、转换、舍入、异常条件和默认处理。实现可以使用硬件、软件或两者结合。

名称常见语言类型总位数符号指数存储 fraction精度
binary32通常是 float32182324 位
binary64通常是 double641115253 位

“通常”很重要。C 和 C++ 的类型名称本身不保证所有平台都使用这些格式。C++ 可通过 std::numeric_limits<T>::is_iec559 等属性检查实现。

binary32 的三个字段

1 位符号 | 8 位指数 | 23 位 fraction

普通有限值的公式是:

(-1)^s × (1.fraction)₂ × 2^(E - 127)

指数使用 127 的 bias。正规化二进制有效数字的开头必定是 1,因此不必实际存储。23 位 fraction 因而提供 24 位精度。binary64 的 bias 是 1023,52 位 fraction 提供 53 位精度。

手工编码 6.25

6.25₁₀ = 110.01₂ = 1.1001₂ × 2²

符号为 0。存储指数是 2 + 127 = 129,即 10000001₂。省略开头的 1 后,fraction 是 1001000...0。

0 10000001 10010000000000000000000

十六进制为 0x40C80000。

float value = 6.25f;
std::uint32_t bits = std::bit_cast<std::uint32_t>(value);

特殊指数模式

指数fraction含义
全为 0全为 0带符号的 0
全为 0非 0subnormal 数
1 到 254任意正规化有限数
全为 1全为 0正或负无穷大
全为 1非 0NaN

因此 binary32 的正规化实际指数范围是 -126 到 127。

正零与负零

指数和 fraction 都为 0 时仍保留符号,所以同时存在 +0 与 -0。普通比较认为两者相等,但某些运算会保留方向。

double p = 0.0;
double n = -0.0;

std::cout << (p == n) << '\n';
std::cout << std::signbit(n) << '\n';
std::cout << 1.0 / p << '\n';
std::cout << 1.0 / n << '\n';

一般 IEEE 行为下,最后两个结果分别为正无穷大和负无穷大。

subnormal 与渐进式下溢

如果靠近 0 时仍要求开头为 1,最小正规化数与 0 之间会留下空隙。存储指数为 0 且 fraction 非 0 时改用:

(-1)^s × (0.fraction)₂ × 2^-126

subnormal 数以逐步失去精度的方式接近 0。binary32 最小正规化正数约为 1.17549435 × 10^-38,最小 subnormal 正数约为 1.40129846 × 10^-45。

部分 SIMD 或 GPU 环境为了性能会把 subnormal 直接视为 0,数值程序必须确认此模式。

无穷大与 NaN

指数全为 1 且 fraction 为 0 时表示无穷大。溢出或非零有限值除以 0 可能在默认处理下产生无穷大。

fraction 非 0 时表示 NaN,Not a Number。0.0 / 0.0 或负数的实数平方根都可能产生 NaN。

NaN 没有普通顺序,甚至与自己比较也不相等。

double nan = std::numeric_limits<double>::quiet_NaN();
std::cout << (nan == nan) << '\n';
std::cout << std::isnan(nan) << '\n';

应使用 std::isnan 判断。quiet NaN、signaling NaN 和 payload 的支持取决于语言与硬件。

为什么 0.1 不能精确表示

0.1 等于 1 / 10。最简分数要成为有限二进制小数,分母的质因数只能有 2。10 还含有 5,因此二进制表示会循环。

0.1₁₀ = 0.000110011001100110011...₂

binary64 只保留 53 位有效数字,其余部分必须舍入。

std::cout << std::setprecision(20) << 0.1 << '\n';

常见结果为 0.10000000000000000555。0.1 + 0.2 的差异来自输入和加法结果各自的舍入。

数值间距随大小增加

浮点数并不均匀分布。binary32 有 24 位精度,因此到 16,777,216 为止的整数都能精确表示。在更大的区域中,相邻值开始相差 2 或更多。

float a = 16'777'216.0f;
float b = a + 1.0f;
std::cout << (a == b) << '\n';

普通 binary32 会输出真,因为该大小附近没有独立的 a + 1。

epsilon 不是万能容差

std::numeric_limits<double>::epsilon() 是 1 与下一个可表示 double 的距离,不适合直接套用到所有大小。实际程序常结合接近 0 时的绝对误差与其他范围的相对误差。

bool almostEqual(double a, double b,
                 double absoluteTolerance = 1e-12,
                 double relativeTolerance = 1e-9)
{
    double difference = std::abs(a - b);
    if (difference <= absoluteTolerance)
    {
        return true;
    }
    double scale = std::max(std::abs(a), std::abs(b));
    return difference <= scale * relativeTolerance;
}

容差必须根据坐标、时间、物理量等问题领域决定。

舍入模式与五种异常状态

精确结果落在两个可表示值之间时,舍入方向决定存储结果。常见默认是取最近值,完全居中时选择最后一位为偶数的一侧。

C 与 C++ 通过 <fenv.h> 和 <cfenv> 访问实现支持的舍入方向和状态标志。

五种 IEEE 异常状态是 invalid operation、division by zero、overflow、underflow、inexact。这通常不是 C++ throw,而是产生默认结果并记录标志。

std::feclearexcept(FE_ALL_EXCEPT);
volatile double one = 1.0;
volatile double zero = 0.0;
volatile double result = one / zero;

if (std::fetestexcept(FE_DIVBYZERO))
{
    std::cout << "division by zero flag\n";
}

运算顺序与累加误差

每个中间结果都会舍入,所以有限精度加法不一定满足结合律。

double large = 1e16;
double small = 1.0;

double first = (large + small) - large;
double second = small + (large - large);

第一个表达式可能丢失 small。大量累加时可以考虑 Kahan summation、相近大小配对相加或使用更宽的累加类型。

金额与精确数据

二进制浮点数很适合图形、坐标和许多科学计算,但通常不适合需要精确十进制舍入的金额。

  • 用整数存储元、分等最小单位。
  • 使用 decimal 或定点类型。
  • 明确规定舍入的时机与方向。

标识符和必须精确的计数器也应使用整数。

序列化与实现检查

跨越文件或网络边界时,应明确 binary32 或 binary64、协议字节序,以及是否保留带符号 0 与 NaN payload。文本往返可用 C++ max_digits10 决定足够位数。

std::numeric_limits 可检查 is_iec559、radix、digits、min()、denorm_min() 和 max()。浮点类型的 min() 是最小正规化正值;最负有限值应查询 lowest()。

参考资料

总结

IEEE 754 使用符号、有效数字和指数,在有限位数内覆盖极大范围。代价是近似、随数值大小改变的间距、特殊值,以及每一步可能发生的舍入。理解同一套模型后,0.1 + 0.2、NaN 比较、大坐标抖动和累加误差就不再是互不相关的怪现象。