Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why is floating point arithmetic not exact?

Yibo
January 16, 2021

Why is floating point arithmetic not exact?

Yibo

January 16, 2021
Tweet

More Decks by Yibo

Other Decks in Programming

Transcript

  1. 先来热个身 6 道⾯试题(阿⾥技术:这 10 道 Java 测试题,据说阿⾥ P7 的正确率只有 50%)

    (1) float a = 0.125f; double b = 0.125d; System.out.println((a - b) == 0.0); 代码的输出结果是什么? A. true B. false
  2. (2) double c = 0.8; double d = 0.7; double

    e = 0.6; 那么 c-d 与 d-e 是否相等? A. true B. false
  3. 答对⼏道? (1) A. true (2) B. false (3) B. Infinity

    (4) C. NaN (5) B. 、C. 、D. 0.1 (6)if(Math.abs(x) < 0.00001f) 2 1030
  4. 2.1 n 位⼆进制可以表示的信息量 对于整数来说,⼤家都知道 8 位有符号整数可以表示 [-128,127] ,8 位⽆符号整数 可以表示

    [0,255] ,不管怎么样,8 位⼆进制⽆论如何也只能表示 256 个整数。当 需要表示 257 这个数,有且只有两个办法: 1. 增加位数,例如 9 位⼆进制可表示的数值范围就可以容纳 257 这个数 2. 改变编码规则,例如规定真值是在机器数的基础上加⼀,这样的话,00000000 就表示数 1,11111111 就表示数 257。(事实上,这就是移码⼲的事情,后边会再提到) 这就是计算机的⾃有属性,数字计算机只能处理离散数据,⼆进制的位数直接决定 了它能表示的离散数据个数,也决定了它所能表示的信息个数,对于 N 位⼆进制 数,它可以表示的信息量为 。 2N
  5. 同理,我们把问题域扩展到全体实数,8 位⼆进制同样也只能表示 256 个实数。假如约定这样⼀ 种 8 位编码:最低两位为⼩数区域,其余是整数区域,这样就有: 000000.00 // 表示

    0.0 000000.01 // 表示 0.25 000000.10 // 表示 0.5 000000.11 // 表示 0.75 000001.00 // 表示 1.0 000001.01 // 表示 1.25 ... 此处省略 250 个数 我们发现,介于 0.0 到 0.25 的数字被跳过了,⽽即使把⼩数区域的位⻓扩⼤到 8 位、16 位、甚⾄ ⼀个极⼤的位数,也⽆法充分表示介于 0.0 到 0.25 所有的数。这是因为,在 0.0 到 0.25 之间的数 是连续的,有⽆限多个数,但是有限的 N 位⻓⼆进制最多只能表示 个信息量,有限的信息量⽆ 法表示⽆限的数据量,这就是现实世界与计算机世界的⽭盾。 2N
  6. 2.2 定点数表示 实数有两种表示格式,分别是定点数和浮点数。像上⾯说的这种约定整数部分和⼩ 数部分为固定位置的格式,就是定点数表示。 • 定义:定点数(fixed point numbers)约定机器数中的⼩数点总是固定在某个特 定的位置。 •

    格式:分为符号位、整数部分、隐含的⼩数点、⼩数部分。 • 特点:整数部分和⼩数部分位⻓固定,当需要表示绝对值特⼤或者特⼩的数需要 很⼤的空间。
  7. 2.3 浮点数表示 我们已经知道 32 位⼆进制可以表示的信息量有 ,但是很多语⾔都会 宣称它们的 32 位单精度浮点数的数值范围约为 ~

    (左右边 界),这是因为采⽤了浮点数格式。 • 定义: 浮点数(floating point numbers)使⽤科学计数法存储数字,⼩数点的位 置根据指数的⼤⼩⽽浮动。 • 格式: 分为符号位、指数、尾数 : • 特点: ⼀部分位作为指数,可以扩⼤所表示的数值范围 • 意义: 是数字计算机表示实数的格式,并以 IEEE 754 (IEEE Standard for Binary Floating-Point Arithmetic) 为标准。 232 ≈ 4 * 109 −3.4 * 1038 3.4 * 1038 N = 2E * M
  8. 2.5.1 转换为⼆进制数格式 这个步骤可能损失精度,换句话说,有些数会损失精度,⽽有些数不会,这取决于 表示这个数需要的信息量和浮点数的存储格式 ⽆理数(⽆限不循环⼩数)包含的信息量是⽆限的,例如圆周率 ,没有任何⼀本 书能够写到圆周率最后⼀位,java.lang.Math.PI 也只是 的近似值,类似的,使⽤ 有限的⼆进制位⾃然⽆法精确表示;

    有限循环⼩数包含的信息量是有限的,它的信息量分为整数部分 + ⼩数不循环部 分 + ⼩数循环部分,例如 。但是浮点数的表示⽅法分为符号 位、指数区域和尾数区域,并不会单独⽤⼀块区域来存储循环的部分,因此有限循 环⼩数也⽆法精确表示; 最后剩下整数和有限⼩数,它们包含的信息量也是有限的,关键看是否有因⼦ 5。 π π 1.8333333... = 1.83
  9. 问:0.1 和 1 万亿,请问哪个数能⽤⼆进制数精确表示? 从⼗进制看,0.1 拥有 2 个信息量(个位数为 0,第⼀位⼩数为 1),1

    万亿拥有⼀ 万亿个信息量,⼆选⼀的话,肯定是选择信息量更低的 0.1。但是,从⼆进制看, 我们会发现 0.1 转换为⼆进制居然是⼀个⽆限循环⼩数 (将整数部分除 2 取余、⼩数部分乘 2 取整来完成转换),所以答案是:1 万亿可以精确表示,⽽ 0.1 ⽆法精确表示! 事实上,在 0.1 到 0.9 的 9 个⼩数中,只有 0.5 可以⽤⼆进制精确的表示。怎么理 解呢?我们把 1 想象成⼀个圆,在⼗进制⾥,它可以划分为 10 等分;但在⼆进制 ⾥,它只能划分为 2 等分。 也就是说⼆进制⾥⼀位,要么表示 0,要么表示⼀半,它没有办法像⼗进制那样表 示 3/10、4/10、6/10...... 1 的⼀半在⼗进制⾥是什么?0.5,所以⼆进制可以精确 表示 0.5,任何包含因⼦ 5 的数都可以⽤⼆进制精确表示。⽆法精确表示的数字, 存储值只能是真实值的近似表示。 0.00011
  10. 2.5.3 转换为 IEEE 754 标准格式 IEEE 754 严格规定了尾数域和指数域可表示的⼤⼩,位数有限,意味着信息量是 有限的。 有些数需要的⼆进制数据量巨⼤,在这个步骤⾃然会损失精度,具体如下:

    • ⼤于浮点数可以表示的最⼤绝对值:上溢(溢出到 ) • ⼩于浮点数可以表示的最⼩绝对值:下溢(溢出到 ) • 尾数有效位数超过尾数域位数(另外还有隐含的整数位 1):舍⼊误差 ±∞ ±0
  11. 3.1 ⼀般格式 浮点数格式的关键是科学计数法格式: 其中: • a 称为尾数 (mantissa),或称有效数字 (significand) •

    B 称为基数 (base),在⼆进制数中,基数是 2 • E 称为指数 (exponent) N = a × BE
  12. ⼀个数的科学计数法表示是不唯⼀的。 举个例⼦,对于⼆进制数 来说,以下都是合法的科学计数法表示: 、 、 ,但这些都不是规格化的表示,唯⼀规格化 的表示为: 对于⼀个科学计数法表示,当尾数 a 的整数部分有且仅有⼀位有效数字时,我们称

    它是规格化的。由于 0 在数字的最 左边是⽆效的,⽽在⼆进制的世 界⾥只有 0 和 1,因此,⼆进制数使⽤规格化的科学计数法时,整数部分固定为 1。 既然整数部分 1 是固定的,那么就没有必要存储整数部分的信息了。正因如此, IEEE 754 标准的浮点数采⽤隐藏位的策略,整数部分的 1 是隐含的,不需要占⽤ ⼀位⽐特,这样是使得尾数可以多⼀位有效数。 1111.0000(2) 111.1 * 2 11.11 * 22 11110 * 2−1 1.111 * 23 1111.0000(2)
  13. IEEE 754 浮点数的⼀般格式: 我们来看这三个区域是如何求值的: • 符号位:0 表示正,1 表示负 • 指数区域:移码

    • 指数区域采⽤移码表示:E = 机器数 - bias,偏移值 bias = 2位⻓ -1 - 1 例如位⻓为 8 时, ,位⻓为 11 时, • 注意:指数域全 0 和全 1 为特殊值 • 尾数区域:隐藏整数位的原码 尾数区域采⽤原码表示: 机器数 举个栗⼦,⼗进制数 转换为⼆进制为 v = (−1)S × 2E−127 × (1 + F) bias = 127 bias = 1023 1.f = 1+ 100(10) 1.100100 * 26 (2) 浮点数转换器
  14. 3.2 两种常⽤格式 前⾯讲的是 IEEE 754 浮点数的⼀般格式,其中最常⽤的是 32 位单精度浮点数 和 64

    位双精度浮点数,在⾼级语⾔中通常代表 float 和 double 两种数据类型(例如 C/C++、Java), 在有些语⾔中只有⼀种数字格式 number(例如 JavaScript/ TypeScript)。 • 单精度 单精度浮点数有 8 位指数,23 位尾数 ,再加上隐藏的整数 1,总共有 24 位⼆进制精 度 • 双精度 双精度浮点数有 11 位指数,52 位尾数,再加上隐藏的整数 1,总共有 53 位⼆进制 精度
  15. 单精和双精浮点数可以保证 7 位和 15 位⼗进制有效数字 log224 = 7.22 log253 =

    15.95 单精和双精浮点数在⼗进制下, 分别可以保证多少位的有效数 字?
  16. 练⼿:将⼗进制数 5.125f 表示为规格化浮点数 整数部分“除 2 取余法”:把整数部分除 2 ,取余,直⾄整数⾄ 0 ,以此类推;

    ⼩数部分“乘 2 取整法”:把⼩数部分乘 2 取整,再把 取完 整数 后 留下的⼩数部分在乘 2 取整,直到⼩数点后为 0 ,以此类推; = = 转为 符号位:0 阶码:2 + 127 = 129,其⼆进制为 1000 0001 尾数:0100 1000 0000 0000 0000 000 结果:0 1000 0001 0100 1000 0000 0000 0000 000 5(10) 101(2) 0.125(10) 0.001(2) 101.001(2) 1.01001 * 22
  17. 练⼿:将⼗进制数 0.26f 表示为规格化浮点数 0.26 转化为⼆进制后为: 0.010000101000111101011100001 = 符号位:0 阶码: -2

    + 127 = 125,⽤⼆进制表示为 0011 1101 尾数: 0000 1010 0011 1101 0111 000 所以结合起来,将 float32 的 0.26 转化为⼆进制表示为: 0 0011 1101 0000 1010 0011 1101 0111 000 1.0000101000111101011100001 * 2−2
  18. 3.3 特殊值 在 IEEE 754 标准规定指数区域全 0 或 全 1

    为特殊值,具体如下: • ⾮规范化数(Denormalized Number) 定义:指数域全 0,尾数域不为 0(去掉隐含整数域为 1 的约定) 意义:可以保存绝对值更⼩的数,所有可表示的浮点数的差值都可以表示 • +0/-0 定义:指数域全 0,尾数域全 0(去掉隐含整数域为 1 的约定)。IEEE 754 未要求具体的尾数域,意味着 NaN 不是⼀个⽽是⼀族。 意义:符号位为 0 是 +0,符号位为 1 是 -0,在涉及⽆穷的运算中避免丢失符号信息,例如 ,如果 0 不区分正负,在 时不成⽴ • 正负⽆穷(Infinity) 定义:指数域全 1,尾数全 0 意义:⽤于表达计算中产⽣的上溢(overflow),使得计算中出现上溢不⾄于终⽌计算 产⽣:除了 NaN 外的⾮零值除以 0,其结果为正负⽆穷 • NaN(Not a Number) 定义:指数域全 1,尾数域不为 0 意义:表示计算中的错误情况,例如 、 ,使得计算中出现错误不⾄于终⽌计算 特点:NaN 是⽆序的,⽐较操作符在任⼀操作数为 NaN 是为 false,!= 在任⼀操作数为 NaN 时为 true,这意味着 NaN != NaN。 1 1/x = x x = ± ∞ 0.0 0.0 −2
  19. float a=0.3f, b=1.6f; a+b=? a=0 0111 1101 0011 0011 0011

    0011 0011 010 0111 1101 b=0 0111 1111 1001 1001 1001 1001 1001 101 0111 1111 1. 对阶 ,即 a 的尾数要右移 2 位 (注意是尾数连同隐藏位⼀起右移) 0 . 01 0011 0011 0011 0011 0011 0 10 2. 尾数运算 0 . 0100 1100 1100 1100 1100 110 (10) + 1 . 1001 1001 1001 1001 1001 101 = 1 . 1110 0110 0110 0110 0110 011 3. 规格化 1.1110 0110 0110 0110 0110 011 已经是个规格化 数据了 4. 舍⼊处理 由于在对阶时,Ma 有右移,且最⾼位为 1,所以按 0 舍 1 ⼊,尾数运算结果调整为 1.1110 0110 0110 0110 0110 100 5. 溢出判断 没有溢出,阶码不调整。 所以最后的结果为 a+b=0 0111 1111 1110 0110 0110 0110 0110 100 = 0011 1111 1111 0011 0011 0011 0011 0100 = =1.900000095367431640625 =1.9000001 Sa = 0 Ea = Sa = 0 Eb = Eb − Ea = 2 (1 + 1 2 + 1 22 + 1 23 + 1 26 + 1 27 + . . . + 1 221 ) * 20
  20. 扩展:Java 的 BigDecimal 是如何解决浮点数精度问题的? 使⽤ BigDecimal 的⼏个注意事项(坑): • ⽐较 BigDecimal

    的值是否相等,必须使⽤ compareTo() ⽽不能使⽤ equals()。 • 尽量使⽤参数类型为String的构造函数。参 数类型为 double 的构造⽅法的结果有⼀定 的不可预知性。 • BigDecimal都是不可变的(immutable) 的, 在进⾏每⼀次四则运算时,都会产⽣ ⼀个新的对象 ,所以在做加减乘除运算时 要记得要保存操作后的值。 package java.math; public class BigDecimal { // 字符串去掉⼩数点后,转为long的值 private final transient long intCompact; // 值的⼩数点后的位数 private final int scale; // 当传的字符串⻓度⼤于等于 18 时才使⽤ BigInteger 表示数字 private final BigInteger intVal; // 值的有效位数,不包含正负符号 private transient int precision; private transient String stringCache; // 加、减、乘、除、绝对值 public BigDecimal add(BigDecimal augend) {} public BigDecimal subtract(BigDecimal subtrahend) {} public BigDecimal multiply(BigDecimal multiplicand) {} public BigDecimal divide(BigDecimal divisor) {} public BigDecimal abs() {} }