从十进制到二进制:深入理解定点数与浮点数表示
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.11
、0.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.75
、000000.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位)结构:
关键特性:
- 阶码偏移:
实际指数 = 阶码 - 127
- 隐含前导1:尾数存储的是小数点后的部分
- 特殊值处理:
-
阶码全0
: 非规格化数(隐含0.xxx) -
阶码全1
: 无穷大/NaN
示例:8.5 = 1000.1 -> 1.0001 * 2³
参考资料
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