浮点数:计算机如何表示小数

什么是浮点数

浮点数(Floating-point)是计算机表示带小数的实数的方式,核心思想是二进制的科学计数法——让小数点位置可以"浮动"。

与之对应的两种表示方式:

类型 英文 小数点位置 示例
整数 Integer 无小数点 00000101 = 5
定点数 Fixed-point 固定在某位后 00101.0100 = 5.25
浮点数 Floating-point 随指数浮动 1.0101 × 2² = 5.25

定点数的小数点位置由硬件或软件约定,始终不变;浮点数则通过指数调整小数点位置,能用同样的位数表示极大或极小的数。

IEEE 754:浮点数的全球标准

几乎所有现代计算机的浮点运算都遵循 IEEE 754(IEEE Standard for Floating-Point Arithmetic),1985 年首次发布,由伯克利分校 William Kahan 教授主导设计(1989 年图灵奖)。

存储结构

一个浮点数被拆分为三部分:

(-1)^符号位 × 1.尾数 × 2^(指数 - 偏移量)
float32: [1位符号] [8位指数] [23位尾数]     共 32 位
float64: [1位符号] [11位指数] [52位尾数]    共 64 位

实例:从二进制还原 float32

所谓「32 位」就是 32 个二进制位(bit),每个位只能是 0 或 1。各类型的位分配如下:

float16:  0 | 00000   | 0000000000          共 16 位
              5位指数    10位尾数

bfloat16: 0 | 00000000 | 0000000            共 16 位
              8位指数     7位尾数

float32:  0 | 00000000 | 00000000000000000000000   共 32 位
              8位指数    23位尾数

float64:  0 | 00000000000 | 0000000000...000       共 64 位
              11位指数      52位尾数

以 3.14 为例,float32 在内存中的 32 位二进制为:

二进制: 0  10000000  10010001111010111000011
        │  ──指数──  ────── 尾数 ──────────────
       符号

逐步还原为十进制:

  1. 符号 = 0 → 正数
  2. 指数 = 10000000₂ = 128,实际指数 = 128 - 127(偏移量)= 1
  3. 尾数 = 10010001111010111000011,加上隐含的前导 1 → 1.10010001111010111000011
  4. 还原:
值 = (-1)^0 × 1.10010001111010111000011₂ × 2^1
   = 1 × 1.10010001111010111000011₂ × 2
   = 11.0010001111010111000011₂   (小数点右移 1 位)
   = 3 + 0.1400001...              (二进制转十进制)
   ≈ 3.14

偏移量(bias) 的作用:float32 的指数范围是 0~255,减去 127 后实际指数为 -127~+128,这样既能表示极大数也能表示极小数。其中 0 和 255 被保留用于特殊值(零、无穷大、NaN)。

常见类型

类型 总位数 符号 指数 尾数 有效精度 范围
float16(半精度) 16 1 5 10 ~3 位 ±65504
bfloat16 16 1 8 7 ~3 位 ±3.4×10³⁸
float32(单精度) 32 1 8 23 ~7 位 ±3.4×10³⁸
float64(双精度) 64 1 11 52 ~15 位 ±1.8×10³⁰⁸

bfloat16 vs float16:bfloat16(Brain Floating Point)由 Google 于 2016 年为 TPU 设计,不是 IEEE 754 标准的一部分,但已成为 AI 训练的事实标准(Intel、NVIDIA、ARM 等硬件均支持)。它的指数位更多(8 vs 5),范围与 float32 相同,但精度更低。

为什么没有 bfloat32? bfloat16 的设计初衷就是做一个「和 float32 范围一样大、但只有 16 位的缩水版」——它保留了 float32 的 8 位指数,只把尾数从 23 位砍到 7 位。如果搞一个 bfloat32,那它就等于 float32 本身,没有意义。所以 bf 前缀只用在 16 位上。

特殊值

IEEE 754 还定义了几个特殊值:

float('inf')     # 正无穷
float('-inf')    # 负无穷
float('nan')     # NaN(Not a Number),如 0/0 的结果

import math
math.isinf(float('inf'))  # True
math.isnan(float('nan'))  # True

精度损失:浮点数的阿喀琉斯之踵

根本原因

IEEE 754 用二进制表示小数,但很多十进制小数在二进制下是无限循环的:

十进制 0.1 = 二进制 0.00011001100110011...(无限循环)
十进制 0.2 = 二进制 0.00110011001100110...(无限循环)

就像十进制无法精确写出 1/3 = 0.3333...一样,二进制也无法精确表示 0.1。浮点数只能截断到有限位,产生微小误差。

经典案例:0.1 + 0.2 ≠ 0.3

>>> 0.1 + 0.2
0.30000000000000004

>>> 0.1 + 0.2 == 0.3
False

原因链:0.1 和 0.2 各自存储时就有微小截断误差 → 两个近似值相加 → 误差叠加 → 结果不再是精确的 0.3。

这不是 Python 的 bug,而是所有使用 IEEE 754 的语言(C/C++/Java/JavaScript/Go/Rust……)的共同行为。

实际存储值

# 用 mpmath 查看 float(0.1) 的真实存储值
from mpmath import mp
mp.dps = 60
mp.mpf(0.1)  # 注意:传入的是 float,不是字符串
# 结果:0.1000000000000000055511151231257827021181583404541015625
# 而不是精确的 0.1

大数吃小数

浮点运算中,当两个数差距很大时,小数可能被"吞掉":

>>> 1e16 + 1.0
1e+16  # 1.0 被吞掉了!

>>> 1e16 + 1.0 == 1e16
True   # 1.0 完全丢失

这是因为 float64 只有 ~15 位有效数字,10000000000000001 需要 17 位才能精确表示,超出了精度上限。

用 Python 深入观察浮点数

查看二进制表示

import struct

# float32 的二进制
data = struct.pack('>f', 3.14)  # '>f' = 大端序 float
print(' '.join(f'{b:08b}' for b in data))
# 01000000 01001000 11110101 11000011
# │符号   │── 指数 ──│──────── 尾数 ────────│

# float64 的二进制
data = struct.pack('>d', 3.14)  # '>d' = 大端序 double
print(' '.join(f'{b:08b}' for b in data))

各精度对比

import struct, math
from mpmath import mp

mp.dps = 50
pi_true = mp.pi

# float32
f32 = struct.unpack('f', struct.pack('f', math.pi))[0]
print(f"float32: {f32:.20f}")

# float64
print(f"float64: {math.pi:.20f}")

# 真值
print(f"真值:    {pi_true}")

# 误差
print(f"f32 误差: {abs(pi_true - mp.mpf(f32))}")
print(f"f64 误差: {abs(pi_true - mp.mpf(math.pi))}")

精确计算方案

# decimal — 十进制精确运算
from decimal import Decimal, getcontext
Decimal('0.1') + Decimal('0.2')  # Decimal('0.3')

# mpmath — 任意精度数学
from mpmath import mp
mp.dps = 100
mp.pi  # π 的 100 位精确值

# fractions — 分数精确运算
from fractions import Fraction
Fraction(1, 3) + Fraction(1, 6)  # Fraction(1, 2)

浮点数与模型量化

深度学习中的模型量化,本质上就是在不同浮点/定点精度之间做权衡:

训练阶段:float32(高精度,慢)
    ↓ 混合精度训练
推理阶段:float16 / bfloat16(中精度,快)
    ↓ 量化
边缘部署:int8 / int4(定点,更快更小)

量化过程

import numpy as np

# 原始权重(float32)
weights = np.array([0.1, -0.5, 0.3, 1.2], dtype=np.float32)

# 量化:float32 → int8
scale = np.max(np.abs(weights)) / 127
quantized = np.round(weights / scale).astype(np.int8)

# 反量化:int8 → float32(有损失)
dequantized = quantized.astype(np.float32) * scale

# 误差
print(f"原始:   {weights}")
print(f"还原:   {dequantized}")
print(f"误差:   {weights - dequantized}")

为什么量化能工作?

神经网络对精度损失有天然的鲁棒性——少量的浮点误差不影响最终的分类/生成结果。量化就是利用这种鲁棒性,用可控的精度损失换取:

浮点运算 vs 定点运算:性能差异

为什么浮点运算理论上更慢?

浮点运算比定点运算(本质是整数运算)多出多个步骤:

定点加法(直接做整数加法,约定小数点位置):

5.25 + 3.75
= 525 + 375(约定小数点后 2 位)
= 900 → 9.00

一步搞定,CPU 的 ALU(算术逻辑单元)原生支持。

浮点加法(需要 4 步):

1.0101 × 2² + 1.1110 × 2¹
 步骤 1:对齐指数 → 1.1110 × 2¹ 变成 0.1111 × 2²
 步骤 2:尾数相加 → 1.0101 + 0.1111 = 10.0100
 步骤 3:规格化   → 10.0100 × 2² 变成 1.00100 × 2³
 步骤 4:舍入     → 截断到有效位数

浮点运算单元(FPU)比整数运算单元(ALU)电路复杂得多,因此在资源受限的硬件上,定点运算更快、更省电。

反直觉:现代 PC 上浮点反而更快

在现代 x86 CPU(Intel/AMD)上,实测结果往往相反:

import numpy as np
import time

N = 10_000_000
f32_a = np.random.randn(N).astype(np.float32)
f32_b = np.random.randn(N).astype(np.float32)
i32_a = np.random.randint(-1000, 1000, N, dtype=np.int32)
i32_b = np.random.randint(-1000, 1000, N, dtype=np.int32)

# 定点模拟:用整数运算 + 缩放因子
SCALE = 1000
fix_a = (f32_a * SCALE).astype(np.int32)
fix_b = (f32_b * SCALE).astype(np.int32)

def bench(op, a, b, label):
    for _ in range(5): op(a, b)  # 预热
    start = time.perf_counter()
    for _ in range(50): op(a, b)
    elapsed = time.perf_counter() - start
    print(f"{label:16s}: {elapsed:.4f}s")

print(f"数组大小: {N:,} 个元素,50 次运算\n")
bench(np.add, f32_a, f32_b, "float32 加法")
bench(np.add, i32_a, i32_b, "int32   加法")
bench(np.add, fix_a, fix_b, "定点(整数) 加法")

print()
bench(np.multiply, f32_a, f32_b, "float32 乘法")
bench(np.multiply, i32_a, i32_b, "int32   乘法")
bench(lambda a,b: np.multiply(a, b) // SCALE, fix_a, fix_b, "定点(整数) 乘法")

x86 PC 实测结果:

运算 float32 int32 定点(整数)
加法 0.53s 0.56s 0.57s
乘法 0.53s 0.63s 1.17s

原因:x86 CPU 有极其强大的 FPU + SIMD(SSE/AVX)指令集,numpy 底层高度优化了 float32 的向量化路径。定点乘法还需额外做一步整数除法 // SCALE,反而更慢。

定点的真正优势在边缘硬件

定点运算的优势不在 PC/GPU,而在 NPU、FPGA、微控制器 等资源受限硬件:

以 RK3588(香橙派)的 NPU 为例:

单元 擅长 float32 算力 int8 算力
CPU(ARM Cortex-A76) 通用计算 中等 中等
GPU(Mali-G610) 图形/浮点 较强 弱
NPU(6 TOPS) AI 推理 弱 极强

NPU 内部堆了大量 int8 乘法器(电路简单,一个加法器就够),但只有很少的 float32 运算单元(电路复杂,要对齐指数、规格化)。因此同一个 NPU 上,int8 算力可以是 float32 的 4~8 倍。

这正是模型量化到 int8 在边缘设备上推理更快的根本原因——不是因为「整数天生快」,而是因为边缘硬件上整数运算单元比浮点单元多得多。

术语速查

中文 英文 含义
浮点数 Floating-point 小数点位置可变的数
定点数 Fixed-point 小数点位置固定的数
整数 Integer 没有小数部分的数
单精度 Single-precision float32,32 位
双精度 Double-precision float64,64 位
半精度 Half-precision float16,16 位
尾数 Mantissa / Significand 有效数字部分
指数 Exponent 2 的幂次
溢出 Overflow 超出最大范围
下溢 Underflow 小于最小可表示值
舍入误差 Rounding error 截断导致的精度损失
大数吃小数 Catastrophic cancellation 差距过大的数相加/减时精度丢失

延伸阅读

相关笔记