Brief
本来只打算理解JS中0.1 + 0.2 == 0.30000000000000004的原因,但发现自己对计算机的数字表示和运算十分陌生,于是只好恶补一下。
本篇我们一起来探讨一下基础——浮点数的表示方式和加减乘除运算。
在深入前有两点我们要明确的:
1. 在同等位数的情况下,浮点数可表示的数值范围比整数的大;
2. 浮点数无法精确表示其数值范围内的所有数值,只能精确表示可用科学计数法m*2 e 表示的数值而已。
(如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*10 38 | 8 | 23 |
Double Precision | 64 | 2.23*10 -308 ~ 1.80*10 308 | 11 | 52 |
为了简便,下面以Single Precision来作叙述。
现在我们了解到32bit的浮点数由3部分组成,那么它们具体又有怎样的组合规则呢?具体分为4种:
Normalized(规格化)
编码规则
1. biased-exponent != 0 && biased-exponent != 2 e -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 == 2 e -1 ;
2. significant == 0。
运算结果为Infinity的表达式
1. Infinity == N/0,其中N为任意正数;
2. -Infinity == N/-0,其中N为任意正数;
3. Infinity == Infinity * N,其中N为任意正数。
NaN(非数值)
用于表示 结果既不是 实数 又不是 无穷。
编码规则
1. biased-exponent == 2 e -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种舍入模式
舍入到最接近且可以表示的值,当存在两个数一样接近时,取偶数值。(如2.4舍入为2,2.6舍入为3;2.5舍入为2,1.5舍入为2。)
Q:为什么会当存在两个数一样接近时,取偶数值呢?
A:由于其他舍入方式均令结果单方向偏移,导致在运算时出现较大的统计偏差。而采用这种偏移则50%的机会偏移两端方向,从而减少偏差。
舍入到最接近且可以表示的值,当存在两个数一样接近时,取离0较远的数值
向正无穷方向舍入
向负无穷方向舍入
向0方向舍入
Overflow
到这里我们对浮点数的表示方式已经有一定程度的了解了,也许你会迫不及待想了解它的运算规则,但在这之前我想大家应该要想理解溢出和如何判断溢出,不然无法理解后续对运算的讲解。
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表示的浮点数。
因此对于有符号整数的运算我们采用进位判断法判断溢出即可,而对于浮点数则需要采用双符号判断法了。
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. View Code
示例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(注意:舍入处理后数值域的最高位是位于隐藏位的) View Code
示例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(注意:舍入处理后数值域的最高位是位于隐藏位的) View Code
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 View Code
注意:计算阶码时,是执行无符号数的加减法。
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 View Code
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 View Code
注意:计算阶码时,是执行无符号数的加减法。
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 View Code
Conclusion
总算写完了:)本文以单精度作为叙述对象,为简化手工运算各示例均以8bit浮点数作为讲解,其实32bit和64bit的浮点数表示和运算规则与其相同,理解规则就OK了。
看完这么多原理性的东西,是时候总结一下我们对浮点数应有的印象了:
1. 浮点数可表示的值范围比同等位数的整数表示方式的值范围要大得多;
2. 浮点数无法精确表示其值范围内的所有数值,而整数则是精确表示其值范围内的每个数值;
3. 浮点数只能精确表示N*2 e 的小数值;
4. 当biased-exponent为2 e-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/
《深入理解计算机系统》