从十进制到二进制:深入理解定点数与浮点数表示


1. 从正整数讲起

从十进制到二进制:深入理解定点数与浮点数表示_十进制

计数法

  • 非位置计数法(Non-positional notation)
  • 罗马数字(Roman numerals)不是位置计数法,因为符号的值是确定的,不依赖位置。
  • VI = 6(V=5 + I=1),但 IV = 4(I before V means subtract, 5-1=4)。
  • 力扣(LeetCode) - 12. 整数转罗马数字
  • 位置/位值计数法(positional notation/place-value notation)
  • 相同的数字在不同位置代表不同值,比如10进制中的 11(第一个1代表10,第二个1代表1)
  • 十进制是以10为底的幂,逢10进1,其权的大小从低到高依次为:10º(1)、10¹(10)、10²(100) …
  • 二进制是以2为底的幂,逢2进1,其权的大小从低到高依次为:2º(1)、2¹(2)、2²(4) …

数值计算

数值的本质上是 “基数的幂次加权和”,即“所有位值(place value)的和”。

大白话就是:每个数位上的数字与其对应的基数幂次相乘后得到位值(place value),累加所有的位值(place value)得到最终的数值。

  • 十进制示例:(13)₁₀
13 = 1 * 10¹ + 3 * 10⁰↑   ↑      ↑   ↑│   │      │   └── 个位:权重为 10⁰(1)│   │      └───── 数值 3│   └── 十位:权重为 10¹(10)└────── 数值 1
  • 二进制示例:(1101)₂
1101 = 1 * 2³ + 1 * 2² + 0 * 2¹ + 1 * 2⁰  ↑   ↑    ↑   ↑    ↑   ↑    ↑   ↑ │   │    │   │    │   │    │   └── 最低位(LSB):权重 2⁰(1)  │   │    │   │    │   │    └────── 数值 1│   │    │   │    │   └── 次低位:权重 2¹(2)  │   │    │   │    └────── 数值 0│   │    │   └── 次高位:权重 2²(4)│   │    └────── 数值 1│   └── 最高位:权重 2³(8)└────── 数值 1
  • 位值由存储位置隐式定义。例如,一个8位二进制数(b0~b7编号) b7b6b5b4b3b2b1b0 的最终数值为:b7*2⁷ + b6*2⁶ + ... + b0*2⁰
  • 数值范围
  • n位十进制正整数范围:0 ~ 10ⁿ - 1
  • n位二进制正整数范围:0 ~ 2ⁿ - 1。这种限制直接体现在数据类型中(如C语言的uint8_t范围为0~255)。

2. 负整数:数学界 vs 计算机底层

数学界的负整数

  • 负整数:与正数表示意义相反的量,与其对应的正数相加后和为0。比如 (-13) + (+13) = 0。
  • 我们需引入正负号(+/-) 的概念,正负号(+/-) 即表示数值的正负性,也可直接参与数值计算。
  • 二进制的加减法规则(下图)与十进制相同(图略):加法=满位则进,减法=不足则借。

计算机的负整数

  • 计算机中的数值计算,不严谨的一般来讲最基础的是"加法"。那么我们怎么用"加法"来表示出减法呢?
  • 我们可以设想一下,由你为计算机上的C语言设计一个数据结构int8_t
    假设正号(+)用1表示,得到负号(-)是8。验证下负号参与数值计算:-8*10⁷ + 9*2⁶ + ... + 1*2⁰ = -70000618,不等于-618,pass。
  • 该数据结构有8bits(可用下标编码为b7b6b5b4b3b2b1b0),我们需要预留1bit给符号位,来表示正负性质。
  • 这个符号位,我们选最高位b7。正号(+)在数学意义上可以不写,那么可以把正号(+)记为0,对应的显式把负号(-)记为1
  • 让我们使用(13)₁₀(0000 1101)₂来推导出 (-13)₁₀ 在计算机中的二进制表示为(1111 0011)₂
0 0 0 0 1 1 0 1				  0 0 0 0 1 1 0 1
+	  x x x x x x x x	--->	+	  1 1 1 1 0 0 1 1
_____________________			_____________________1 0 0 0 0 0 0 0 0				1 0 0 0 0 0 0 0 0

这个推导出来的负数表示方式,就是计算机中的:补码 = 源码取反 + 1

  • 为什么要源码取反 + 1?当我们在谈论取反时在谈论什么?这又要回到负数的特性:与其对应的正数相加后和为0。
  • 那么你现在只有8bits,两个非0数相加只有一种情况能等于0:溢出!那么我可以先达到临界值,然后再+1,就刚好溢出了!
0 0 0 0 1 1 0 1				  0 0 0 0 1 1 0 1				  0 0 0 0 1 1 0 1				  0 0 0 0 1 1 0 1
+	  x x x x x x x x	--->	+	  x x x x x x x y	--->	+	  1 1 1 1 0 0 1 0	--->	+	  1 1 1 1 0 0 1 1
_____________________			_____________________			_____________________			_____________________1 0 0 0 0 0 0 0 0				  1 1 1 1 1 1 1 1				  1 1 1 1 1 1 1 1				1 0 0 0 0 0 0 0 0+	                1			+	                1_____________________			_____________________1 0 0 0 0 0 0 0 0				1 0 0 0 0 0 0 0 0
  • 补码的引入,使得加减法统一为加法操作。
  • 补码的符号位也参与数值表示,比如(1111 0011)₂的数值为:-1*2⁷ + 1*2⁶ + 1*2⁵ + 1*2⁴ + 0*2³ + 0*2² + 1*2¹ + 1*2⁰ = -(1101)₂ = -(13)₁₀
  • 你也可以理解为这8位表示了一个未知数-X
  • -1*2⁷ + (1*2⁶ + 1*2⁵ + 1*2⁴ + 0*2³ + 0*2² + 1*2¹ + 1*2⁰) = -X
  • (1*2⁶ + 1*2⁵ + 1*2⁴ + 0*2³ + 0*2² + 1*2¹ + 1*2⁰) + X = 2⁷
  • X=(1101)₂,未知数 -X=(-13)₁₀
  • 你还可以理解为:数据位表示周期数据的最大值+1(溢出值),然后负数等于周期内互补的正数。比如12进制中,我们想表示3-3=0,既可以直接-3,也可以+9。
  • 如果有12进制的计算机,负数-3可能表示为(19)₁₂
  • -(1*12¹)₁₂ + (9*12⁰)₁₂ = -(X)₁₂
  • (9*12⁰)₁₂+ (X)₁₂ = (1*12¹)₁₂
  • X=(3)₁₂,未知数-X=(-3)₁₀

3. 小数(数学概念,仅举例正数)

数值计算,和整数一样,只是扩展了小数部分,其每位的权重指数级递减。

  • 十进制
13.75 = 1 * 10¹ + 3 * 10⁰ + 7 * 10⁻¹ + 5 * 10⁻²↑   ↑      ↑   ↑│   │	   │   └── 百分位:权重为 10⁻²(1/100 = 0.01)│   │      └───── 数值 5│   └── 十分位:权重为 10⁻¹(1/10 = 0.1)└────── 数值 7
  • 二进制
1101.11 = 1 * 2³ + 1 * 2² + 0 * 2¹ + 1 * 2⁰ + 1 * 2⁻¹ + 1 * 2⁻²↑   ↑    ↑   ↑ │   │    │   └── -2位:权重 2⁻²(1/4 = 0.25)  │   │    └────── 数值 1│   └── -1位:权重 2⁻¹(1/2 = 0.5)  └────── 数值 1
  • 十进制小数无法全部转为二进制小数,比如(0.1)₁₀ → (0.0001100110011...)₂(无限循环)
  • 因为整数部分的位权最终都会拆分到1,是有唯一解的。而小数部分的位权是可以无限小的,拆分剩余的会留给后序位继续拆,可能会无线循环。

4. 计算机中的定点数

二进制定点数格式

  • 我们以8位定点数格式 iiiiii.ff举例, 小数点位置是固定的,始终表示整数位有6位,小数位有2位。其表示方式和数学概念上的小数一致,遵循位置/位值计数法。
  • 我们内部会称这样的排布为U6.2 ,表示无符号位+6位整数位+2位小数位
  • 数值最小值:0.0,数值最大值:(2⁶ - 1) + (1 - 2⁻²) = 63.75

十进制小数 转 二进制定点数

  • 整数部分直接转,小数部分需要逐级拆,可能会精度损失。
  • 精度无损:
  • 口算版本:(13.75)₁₀ = 001101.11(0.5)₁₀ = 000000.10
  • 二进制中拆小数实在太麻烦了,那么我们可以直接以整数的视角来看数据,把小数点移动到最右,这样数据需要乘以2的小数位次方。
  • 代码计算版:13.75 * 2² -> 55.0 = (只表示整数位)00110111 -> 001101.110.5 * 2² -> 2 = 00000010 -> 000000.10
  • 精度丢失:
  • 口算13.875 = 001101.111 但我们的定点数小数位只有2位,第三位会被舍弃,最终为 001101.11
  • 代码计算:13.875 * 2² -> 55.5 = (只表示整数位)00110111 -> 001101.11
  • 转换时,精度大概率会损失。那么我们要制定一些精度损失时的策略:
  • HALF_UP:floor(value * 2ⁿ + 0.5)。eg. floor(13.875 * 2² + 0.5) -> 56 = 00111000 -> 001110.00
  • HALF_DOWN:-floor(-value * 2ⁿ + 0.5)。eg. -floor(-13.875 * 2² + 0.5) -> 55 = 00110111 -> 001101.11

二进制定点数 转 十进制小数

  • 无脑转即可,不存在精度问题。
  • 口算:001101.11 = (13.75)₁₀000000.10 = (0.5)₁₀
  • 代码计算:001101.11 * 2² -> 00110111 = 55 / 2² -> 13.75000000.10 * 2² -> 00000010 = 2 / 2² -> 0.5

5. 科学计数法 和 IEEE 754

十进制科学计数法:

  • 以首位不为0的数做个位,后续位做小数位构成尾数M,乘以10的E次幂,即 N = (-1)^s × M × 10^E
  • s: 符号(0正1负)
  • M: 尾数(1 ≤ M < 10)
  • E: 阶码
  • 示例:13.75 → 1.375 * 10¹0.618 -> 6.18 * 10⁻¹

二进制科学计数法

  • 和十进制一样,以首位不为0的数做个位(只能是1),后续位做小数位构成尾数1.M,乘以2的E次幂,即 N = (-1)^s × 1.M × 2^E
  • 示例:1101.11 -> 1.10111 * 2³

IEEE 754

单精度(32位)结构:
关键特性:
  1. 阶码偏移实际指数 = 阶码 - 127
  2. 隐含前导1:尾数存储的是小数点后的部分
  3. 特殊值处理
  • 阶码全0: 非规格化数(隐含0.xxx)
  • 阶码全1: 无穷大/NaN

示例:8.5 = 1000.1 -> 1.0001 * 2³

从十进制到二进制:深入理解定点数与浮点数表示_二进制_02

参考资料

https://medium.com/starbugs/see-why-floating-point-error-can-not-be-avoided-from-ieee-754-809720b32175

https://www.h-schmidt.net/FloatConverter/IEEE754.html
从十进制到二进制:深入理解定点数与浮点数表示_算法_03