基础野:细说浮点数

Brief                              

本来只打算理解JS中0.1 + 0.2 == 0.30000000000000004的原因,但发现自己对计算机的数字表示和运算十分陌生,于是只好恶补一下。

本篇我们一起来探讨一下基础——浮点数的表示方式和加减乘除运算。

在深入前有两点我们要明确的:

1. 在同等位数的情况下,浮点数可表示的数值范围比整数的大;

2. 浮点数无法精确表示其数值范围内的所有数值,只能精确表示可用科学计数法m*2e表示的数值而已。

(如0.5的科学计数法是2-1,则可被精确存储;而0.2则无法被精确存储)

Encode                            

20世纪80年代前每个计算机制造商都自定义自己的表示浮点数的规则,及浮点数执行运算的细节。而且不太关注运算的精确性,而是更多地关注速度和简便性。

1985年左右推出IEEE 754标准的浮点数表示和运算规则,才让浮点数的表示和运算均有可移植性。(IEEE,读作Eye-Triple-Eee,电器与电子工程师协会)

上述的IEEE 754称为IEEE 754-1985 Floating point,直到2008年对其进行改进得到我们现在使用的IEEE 754-2008 Floating point标准。

IEEE 754的二进制编码由3部分组成,分别是sign-bit(符号位),biased-exponent(基于偏移的阶码域)和significant(尾数/有效数域)。

Sign-bit:0表示正,1表示负。占1bit;

Biased-exponent:首先exponent表示该域用于表示指数,也就是数值可表示数值范围,而biased则表示它采用偏移的编码方式。那么什么是采用偏移的编码方式呢?也就是位模式中没有设立sign-bit,而是通过设置一个中间值作为0,小于该中间值则为负数,大于改中间值则为正数。IEEE 754中规定bias = 2^e-1 - 1,e为Biased-exponent所占位数;
     Significant:表示有效数,也就是数值可表示的精度。(注意:Significant采用原码编码)。
     另外IEEE 754还提供4个精度级别的浮点数定义(单精度、双精度、延生单精度和延生双精度),单精度和双精度具体定义如下:

Level Width(bit) Range at full precision Width of biased-exponent(bit) Width of significant(bit)
Single Precision 32 1.18*10-38 ~ 3.4*1038 8 23
Double Precision 64 2.23*10-308 ~ 1.80*10308 11 52

为了简便,下面以Single Precision来作叙述。
    

现在我们了解到32bit的浮点数由3部分组成,那么它们具体又有怎样的组合规则呢?具体分为4种:

Normalized(规格化)

编码规则

1. biased-exponent != 0 && biased-exponent != 2e-1;

2. exponent = biased-exponent - Bias,其中exponent是指实际的指数值;

3. significant前默认含数值为1的隐藏位(implied leading 1),即若significant域存储的是0001,而实际值是10001。

Denormalized(非规格化)

用于表示非常接近0的数值和+0和-0。

    +0的位模式为:0-00000000-00000000000000000000000

    -0的位模式为: 1-00000000-00000000000000000000000

    编码规则

     1. biased-exponent == 0 ;

2. exponent = 1 - Bias,其中exponent是指实际的指数值;

3. significant前默认含数值为0的隐藏位(implied leading 1),即若significant域存储的是0001,而实际值是00001。

Infinity(无限大)

用于表示溢出。

编码规则

     1. biased-exponent == 2e-1 ;

2. significant == 0。

运算结果为Infinity的表达式

1. Infinity == N/0,其中N为任意正数;

2. -Infinity == N/-0,其中N为任意正数;

    3. Infinity == Infinity * N,其中N为任意正数。

NaN(非数值)

   用于表示 结果既不是 实数 又不是 无穷。

编码规则

     1. biased-exponent == 2e-1 ;

2. significant != 0。

   运算结果为NaN的表达式

     1. Math.sqrt(-1)

     2. Infinity - Infinity

     3. Infinity * 0

       4. 0/0

注意

    1. NaN与任何数值作运算,结果均为NaN;

    2. NaN != NaN;

    3. 1 == Math.pow(NaN, 0)。

Rounding modes(aka Rounding scheme,舍入模式)

由于浮点数无法精确表示所有数值,因此在存储前必须对数值作舍入操作。具体分为以下5种舍入模式

  1. Round to nearest, ties to even(IEEE 754默认的舍入模式)

    舍入到最接近且可以表示的值,当存在两个数一样接近时,取偶数值。(如2.4舍入为2,2.6舍入为3;2.5舍入为2,1.5舍入为2。)

Q:为什么会当存在两个数一样接近时,取偶数值呢?

      A:由于其他舍入方式均令结果单方向偏移,导致在运算时出现较大的统计偏差。而采用这种偏移则50%的机会偏移两端方向,从而减少偏差。

  2. Round to nearest, ties to zero

    舍入到最接近且可以表示的值,当存在两个数一样接近时,取离0较远的数值

3. Round to infinity

    向正无穷方向舍入

4. Round to negative infinity

    向负无穷方向舍入

  5. Round to zero

    向0方向舍入

Overflow                            

到这里我们对浮点数的表示方式已经有一定程度的了解了,也许你会迫不及待想了解它的运算规则,但在这之前我想大家应该要想理解溢出和如何判断溢出,不然无法理解后续对运算的讲解。

Q1:何为溢出?

A1:溢出,就是运算结果超出可表示的数值范围。注意:进位、借位不一定会产生溢出。

发生溢出:
4位有符号整数 7+7
  0111
+ 0111
  1110 => -2
只有最高数值位发生进位,因此发生溢出

没有发生溢出:
4位有符号整数 7-2
  0111
+ 1110
 10101 => 取模后得到5
符号位和最高数值位均发生进位,因此没有发生溢出

Q2:如何判断溢出?

A2:有两种方式判断溢出,分别是 进位判断法 和 双符号判断法。

进位判断法

      前提:采用单符号位,即+4的二进制位模式为0100。

    无溢出:符号位 和 数值域最高位 一起进位或一起不发生进位。

溢出:符号位 或 数值域最高位 其中一个发生进位。

双符号判断法

    前提:采用双符号位,即+4的二进制位模式为00100。

无溢出:两符号位相同。

上溢出:两符号位为01。

下溢出:两符号位为10。

Q3:溢出在有符号整数和浮点数间的区别?

A3:对于有符号整数而言,溢出意味着运算结果将与期待值不同从而导致错误;

对于浮点数而言,会对上溢出和下溢出进行特殊处理,从而返回一个可被IEEE 754表示的浮点数。

因此对于有符号整数的运算我们采用进位判断法判断溢出即可,而对于浮点数则需要采用双符号判断法了。

Q4:浮点数运算中上溢出和下溢出具体的特殊处理是什么啊?

A4:首先浮点数运算中仅对阶码进行溢出判断,当阶码出现下溢出时运算结果为0(符号取决于符号位);当阶码出现上溢出时运算结果为Infinity(符号取决于符号位)。

PS:发生溢出时,当前程序会被挂起,并发送溢出中断信号量,此时溢出中断处理程序接收信号量后会做对应的处理。

Addition & Subtraction                    

恭喜你还没被上述的前置知识搞晕而选择X掉网页,下面我们终于可以着手处理运算问题,顺便验证一下自己对上述内容是否真的理解了。

步骤:

  1. 对0、Infinity和NaN操作数作检查

    若有一个操作数为NaN则直接返回NaN;

    若有一个操作数为0则直接返回另一个操作数;

    若有一个操作数为Infinity,

      若另一个操作数也是Infinity且符号相同则返回第一个操作数;

      若另一个操作数也是Infinity且符号不同则返回NaN;

      若其他情况则返回Infinity。

2. 对阶,若两操作数的阶码不相等,则对阶(小数点对齐)。

     由于尾数右移所损失的精度比左移的小,因此小阶向大阶看齐。

3. 符号位+尾数(包含隐藏位)执行加减法。

按有符号整数加减法进行运算,并采用双符号法判断是否溢出。

4. 规格化。

向右规格化:若步骤3出现溢出,则尾数右移1位,阶码+1;

向左规格化:若步骤3没有出现溢出,且数值域最高位与符号位数值相同,则尾数左移1位且阶码-1,直到数值域最高位为1为止。

5. 舍入处理

   6. 溢出判断

     由于规格化时可能会导致阶码发生溢出

若无发生溢出,则运算正常结束。

若发生上溢出,则返回Infinity。

若发生下溢出,则返回0。

示例1,0.75+(-0.75) = 0:

0.75+(-0.75) = 0

以8位存储,尾数采用原码编码
0.75  存储位模式 0-0011-100
-0.75 存储位模式 1-0011-100

1. 对阶
阶码一样跳过。

2. 符号位+尾数(含隐藏位)相加
由于尾数以有符号数的方式进行运算,因此要对尾数进行取补操作
  00-1100
+11-0100
 100-0000
 对符号位截断后得到00-0000

3. 规格化
根据符号位相同,则执行左规格化操作,也就是尾数不断向左移而阶码不断减1,直到尾数最高位为1为止。

4. 舍入处理
000

5.溢出判断
  在执行规格化时发生阶码下溢出,整体结果返回0-0000-000.

示例2,0.75-0.25 = 0.5:

0.75-0.25 = 0.5

以8位存储,尾数采用原码编码
0.75  存储位模式 0-0011-100
0.25  存储位模式 0-0010-000

1. 对阶
0.25的阶码小于0.75相同,因此0.25向0.75的看齐。
0-0011-100

2. 尾数相加(采用双位符号法),减法转换为加法,对0.75和-0.25取补
  00-1100
+11-1100
 100-1000
 对符号位截断后得到00-1000

3. 规格化
根据符号位相同,则执行左规格化操作,也就是尾数不断向左移而阶码不断减1,直到尾数最高位为1为止。

4. 舍入处理
1000

5. 溢出判断
阶码没有发生溢出,正常返回运算结果0-0011-000(注意:舍入处理后数值域的最高位是位于隐藏位的)

示例3, 0.25-0.75 = -0.5:

0.25-0.75 = -0.5

以8位存储,尾数采用原码编码
0.25  存储位模式 0-0010-000
0.75  存储位模式 0-0011-100

1. 对阶
0.25的阶码小于0.75相同,因此0.25向0.75的看齐。
0-0011-100

2. 尾数相加(采用双位符号法),减法转换为加法,对0.25和-0.75取补
  00-0100
+11-0100
  11-1000
 对符号位截断后得到11-1000

3. 规格化
根据符号位相同,则执行左规格化操作,也就是尾数不断向左移而阶码不断减1,直到尾数最高位为1为止。

4. 舍入处理
1000

5. 溢出判断
阶码没有发生溢出,正常返回运算结果1-0011-000(注意:舍入处理后数值域的最高位是位于隐藏位的)

Comparison                          

比较运算(cmp指令)实际上就是对两操作数做减法,然后通过标志寄存器(80x86的rflags)中的CF(Carry flag)、ZF(Zero flag)、OF(Overflow flag)、SF(Sign flag)状态标志来判断两者的关系。

对于无符号数A与B而言,则是通过CF和ZF来判断。

1. 若CF为1,表示A-B时A发生借位操作,那么可以判定 A<B

2. 若CF为0且ZF不为0,表示A-B时没有发生借位操作,那么可以判定 A>B

3. 若ZF为0,则可判定A==B

对于有符号数C和D而言,则是通过ZF、OF和SF来判断。

1. 若ZF为1,则可判定C == D

2. 若SF为0,OF为0,表示结果为正数且没有发生溢出,则C>D

3. 若SF为0,OF为1,表示结果为正数且发生溢出,则C<D

4. 若SF为1,OF为0,表示结果为负数且没有发生溢出,则C<D

5. 若SF为1,OF为1,表示结果为负数且发生溢出,则C>D

而对于浮点数而言,由于阶码域采用的是biased-exponent的编码方式,因此在进行比较时我们可以将整个浮点数看作有符号数来执行减法运算即可,而不必执行Addition/Subtraction那样繁琐的运算步骤。

Multiplication                        

步骤:

  1. 对0、Infinity和NaN操作数作检查

    若有一个操作数为NaN则直接返回NaN;

    若有一个操作数为0则直接返回另一个操作数;

    若有一个操作数为Infinity,

      若另一个操作数也是Infinity且符号相同则返回第一个操作数;

      若另一个操作数也是Infinity且符号不同则返回NaN;

      若其他情况则返回Infinity。

   2. 计算阶码(公式:e = e1 + e2 - Bias)

     公式推导过程:

∵ E1 = e1 - Bias
∵ E2 = e2 - Bias
∴ E = E1+E2 = e1 + e2 - 2*Bias
∵ e = E + Bias

∴ e = e1 + e2 - Bias
对非规格化e1为1

注意:计算阶码时,是执行无符号数的加减法。

3. 尾数相乘

注意:尾数相乘时,是执行无符号数的乘法,并且不对结果进行截断。

4. 结果左规格化,尾数左移,且阶码减1,直到最高位为1为止

   5. 舍入处理

   6. 溢出判断

     由于规格化时可能会导致阶码发生溢出

若无发生溢出,则运算正常结束。

若发生上溢出,则返回Infinity。

若发生下溢出,则返回0。

   7. 符号位执行 异或 运算

示例,0.5*(-0.25) = -0.125:

0.5*(-0.25) = -0.125

以8位存储,尾数采用原码编码
0.5  存储位模式 0-0011-000
-0.25  存储位模式 1-0010-000

1. 计算阶码(公式e1+e2-Bias)
  0011
+0010
  0101
+1001
  1110 取模得到 0010

2. 尾数相乘
1000*1000
1000<<(3+1) - 1000<<(3)
  1000000
- 0100000
减法转换为加法
  1000000
+1100000
10100000 取模得到0100000

3. 左规格化
0100000左移1位得到 100000
阶码-1得到 0001

4. 舍入处理
尾数为1000

5. 溢出判断
无溢出

6. 符号位异
0 xor 1 = 1

结果
1-0001-000

Divsion                            

步骤:

  1. 对0、Infinity和NaN操作数作检查

    若有一个操作数为NaN则直接返回NaN;

    若有一个操作数为0则直接返回另一个操作数;

    若有一个操作数为Infinity,

      若另一个操作数也是Infinity且符号相同则返回第一个操作数;

      若另一个操作数也是Infinity且符号不同则返回NaN;

      若其他情况则返回Infinity。

   2. 计算阶码(公式:e = e1 - e2 + Bias)

     公式推导过程:

∵E1 = e1 - Bias
∴E2 = e2 - Bias
∴E = E1-E2 = e1 - e2
∵e = E + Bias

∴e = e1 - e2 + Bias
对非规格化e1、e2为1

注意:计算阶码时,是执行无符号数的加减法。

3. 尾数相除

注意:尾数相除时,是执行无符号数的除法,并且不对结果进行截断。

4. 结果左规格化,尾数左移,且阶码减1,直到最高位为1为止

   5. 舍入处理

   6. 溢出判断

     由于规格化时可能会导致阶码发生溢出

若无发生溢出,则运算正常结束。

若发生上溢出,则返回Infinity。

若发生下溢出,则返回0。

   7. 符号位执行 异或 运算

示例,0.5/(-0.25) = -2:

0.5/(-0.25) = -2

以8位存储,尾数采用原码编码
0.5  存储位模式 0-0011-000
-0.25  存储位模式 1-0010-000

2. 计算阶码(公式e1-e2+Bias)
  0011
+1110
  10001
+00111
  11000 取模得到 1000

3. 尾数相除
1000/1000 = 1000 >> 3 得到0001

4. 左规格化
0001左移3位得到 1000
阶码-3得到 0101

5. 舍入处理
尾数为1000

6. 溢出判断
无溢出

7. 符号位异
0 xor 1 = 1

结果
1-0101-000

Conclusion                          

总算写完了:)本文以单精度作为叙述对象,为简化手工运算各示例均以8bit浮点数作为讲解,其实32bit和64bit的浮点数表示和运算规则与其相同,理解规则就OK了。

看完这么多原理性的东西,是时候总结一下我们对浮点数应有的印象了:

1. 浮点数可表示的值范围比同等位数的整数表示方式的值范围要大得多;

2. 浮点数无法精确表示其值范围内的所有数值,而整数则是精确表示其值范围内的每个数值;

3. 浮点数只能精确表示N*2e的小数值;

4. 当biased-exponent为2e-1-1时,浮点数所表示的就是整数了。

尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/5109766.html 肥子John^_^

Thanks                            

https://en.wikipedia.org/wiki/IEEE_754-1985

http://geeklu.com/2011/03/ieee754-floating-point-arithmetic/

http://blog.csdn.net/hillchan31/article/details/7565782

http://blog.csdn.net/jn1158359135/article/details/7761011

http://laokaddk.blog.51cto.com/368606/284280/

《深入理解计算机系统》

时间: 2024-08-04 03:57:59

基础野:细说浮点数的相关文章

基础野:细说原码、反码和补码

Brief 说来惭愧虽然刚接触计算机时已经学过原码.反码和补码的内容,但最近重温时却发现“这是什么鬼东西”,看来当初只是应付了考试了而已.本篇将试图把他们说个明白,以防日后自己又忘记了. 在深入之前,我们先明确以下几点: 1. 本篇内容全部针对有符号数整数: 2. 对于有符号数整数,其在计算机中的存储结构是 符号位 + 真值域.其中符号位为0表示正数,1表示负数: 3. Q:既然已经有原码,那么为什么还要出现反码.补码等数值的编码方式呢? A:由于为了降低当时计算机物理电路的设计难度,决定采用加

Python3基础 input 输入浮点数,整数,字符串

? ???????Python : 3.7.0 ?????????OS : Ubuntu 18.04.1 LTS ????????IDE : PyCharm 2018.2.4 ??????Conda : 4.5.11 ???typesetting : Markdown ? code """ @Author : 行初心 @Date : 18-9-24 @Blog : www.cnblogs.com/xingchuxin @Gitee : gitee.com/zhichengji

【Java面向对象基础(二)】细说String、StringBuffer和StringBuilder

[喵"的Android之路][基础篇(二)][Java面向对象基础]细说String.StringBuffer和StringBuilder 1.String String是Java中的一个final类,主要用于字符串的处理. 1.1 不可变性 String内的字符串是不可变的,每一次修改都会重新生成一个新的String对象实例. 例: 1 // 在堆中会创建一个"Hello"字符串实例,把地址赋给对象a 2 String a = new String("Hello&

社交APP经典死法18种,听野路子产品菜狗怎么说

点这里 社交APP经典死法18种,听野路子产品菜狗怎么说 时间 2015-04-06 11:24:53  虎嗅网相似文章 (4)原文  http://www.huxiu.com/article/112016/1.html 自从2004年,一个乳臭未干的哈佛大二宅男闷着头做出世界上最大的社交网络以来,SNS社交网络就一直是大众关注的要点.在移动互联网时代,SNS几乎是投资界风险最高但一旦成功却收益最大的领域.尽管很多人已经表示2015年不再看好SNS,但这并不妨碍大量的热钱向这里集中. 人们看好社

Java基础篇(01):基本数据类型,核心点整理

本文源码:GitHub·点这里 || GitEE·点这里 一.基本类型 1.基本类型 不使用New创建,声明一个非引用传递的变量,且变量的值直接置于堆栈中,大小不随运行环境变化,效率更高.使用new创建的引用对象存储在堆中. 2.基本信息 基本类型包括如下几种:byte.short.int.long.float.double.boolean.char,可以通过相关方法查看范围大小. public class IntType01 { public static void main(String[]

JS魔法堂:再识Number type

Brief 本来只打算理解JS中0.1 + 0.2 == 0.30000000000000004的原因,但发现自己对计算机的数字表示和运算十分陌生,于是只好恶补一下.以下是恶补后的成果: 基础野:细说原码.反码和补码(http://www.cnblogs.com/fsjohnhuang/p/5060242.html) 基础野:细说无符号整数(http://www.cnblogs.com/fsjohnhuang/p/5078290.html) 基础野:细说有符号整数(http://www.cnbl

JS魔法堂:彻底理解0.1 + 0.2 === 0.30000000000000004的背后

Brief 一天有个朋友问我“JS中计算0.7 * 180怎么会等于125.99999999998,坑也太多了吧!”那时我猜测是二进制表示数值时发生round-off error所导致,但并不清楚具体是如何导致,并且有什么方法去规避.于是用了3周时间静下心把这个问题搞懂,在学习的过程中还发现不仅0.7 * 180==125.99999999998,还有以下的坑 1. 著名的 0.1 + 0.2 === 0.30000000000000004 2. 1000000000000000128 ===

精选架构技术文章和总结来袭

大家都是我的老朋友了,所以今天就来点干货.当然,奉献给大家首先是技术总结(电子书),对着手机看文章,不急,看不完先收着,以后慢慢看. 笔者通过总结.归类和细化公众号历史原创文章,并梳理成电子书,电子书打包的目录如下: <RDMA原理分析.对比和技术实现解析><数据备份和副本管理技术全面解析><容器技术架构.网络和生态详解><闪存技术.产品和发展趋势全面解析><虚拟化技术最详细解析><传统企业存储知识完全解析><IO知识和系统性能

java web 开发三剑客 -------电子书

Internet,人们通常称为因特网,是当今世界上覆盖面最大和应用最广泛的网络.根据英语构词法,Internet是Inter + net,Inter-作为前缀在英语中表示“在一起,交互”,由此可知Internet的目的是让各个net交互.所以,Internet实质上是将世界上各个国家.各个网络运营商的多个网络相互连接构成的一个全球范围内的统一网,使各个网络之间能够相互到达.各个国家和运营商构建网络采用的底层技术和实现可能各不相同,但只要采用统一的上层协议(TCP/IP)就可以通过Internet