前言
最近在学习孤尽大神内网分享的资料时,被安利了一本书【码出高效】,这也是他在【阿里巴巴Java开发手册】一书后,结合阿里的生产经验和历史故障给出的最佳研发实践。
刚拿到书,打开目录,会觉得这是一本基础入门书,里面的东西大家都懂,但是没翻几页你就会被里面生动的案例和技术细节吸引,有很多在日常开发中会经常用到,但一直被忽略的点,这些点往往是引发故障的点。现在就将我在学习过程中的笔记和收获记录下来和大家一起分享。
本篇就从计算机入门的二进制和浮点数开始。
二进制
起源
简单来说,计算机是由晶体管和电路板组合起来的电子设备,信息存储和逻辑计算的元数据归根结底都是0和1的信号处理。
只有0和1,进位规则是逢二进一,借位规则是借一当二,这就是二进制。
编码方式
通过符号位和数字实际值可以表示数,有以下三种基本编码方式
原码
正数部分是数值本身,符号位为0;负数数值部分是数值本身,符合位为1。八位二进制数表示范围是[-127, 127]。这是最符合人类认知的编码方式。
反码
正数部分是数值本身,符号位为0;负数数值部分是正数的基础上对各位取反,符号位为1。八位二进制的表示范围是[-127, 127]。
补码
正数部分是数值本身,符号位为0;负数树值部分是正数的基础上对各位取反后加一,符号位为1.八位二进制的表示范围是[-128, 127]。
加减运算
因为计算机中只有加法器,没有减法器。通过原码相加,结果会出错。如:
1 - 2 = 1 + ( -2 ) = -1
通过原码计算为[0000 0001] + [1000 0010] = [1000 0011] = -3,结果明显是不对的。
通过反码计算为[0000 0001] + [1111 1101] = [1111 1110] = -1,结果正确。
但是反码在某些情况出现新的问题。如:
2 - 2 = 2 + ( -2 ) = 0
通过原码计算为[0000 0010] + [1000 0010] = [1000 0100] = -4,结果不正确。
通过反码计算为[0000 0010] + [1111 1101] = [1111 1111] = -0,反码有+0和-0的区分,用反码计算也是有歧义的。
通过补码计算为[0000 0010] + [1111 1110] = [0000 0000] = 0,补码中只有0,结果符合预期。
加减法是一个高频运算,使用同一个运算器,可以减少中间变量的存储及转换成本,也降低了CPU设计的复杂度。
位运算
左移:<<,相当于乘2
右移:>>,相当于除2(最后一位是奇数时,存在误差),带符号位右移动,负数最高位补1,正数最高位补0。所以可以通过使用 ((b >> 31) ^ (a >> 31)) == 0 来判断两个整数正负是否相同。
无符号右移:>>>,无符号位右移,正数和负数最高位都补0,主要用于一些数据转换,如加密、压缩、影音编码等场景。
取反:~,按位取反,0取反是1
与:&,按位与,相同位都为1才,就为1
或:|,按位或,相同位只要有一个是1,就为1
异或:^,按位异或,相同位相等的时候为1,不相同的时候为0
浮点数
浮点数采用科学技术法来表示的,由符号位、有效数字、指数三部分组成。但编码过程中经常会出现丢失精度的问题,如下:
float a = 1f;
float b = 0.9f;
float c = (a - b);
//c = 0.100000024
System.out.println(c);
常见的浮点数有单精度和双精度浮点数,占用字节数和取值范围不同。单精度四个字节,双精度八个字节。
单精度浮点数的构成是1位符号位,8位阶码位(指数),23位尾数位(有效数字)。
阶码使用移码来表示,尾数使用原码来表示。移码范围是[0, 255]去掉特殊的0(计算机认为全零是机器0)和255(计算机认为是无穷大),阶码的取值范围是[1, 254],根据移码的定义,[x]=x+2^(n-1),n是8,所以x的取值范围是[-126, 127]
尾数位是原码表示的,1.111....111(23个1), 1 <= a < 2, 因此规格化后的尾数首个1会被省略,因此实际能表示24位尾数,最大值无限接近于2,因此单精度浮点数能表示的最大值为2^127,约等于(1.7*10^38)。
加减运算
小数加减需要将小数点对齐后进行同位相加减,因此浮点数加减需要先将指数对齐,再进行同位加减
零值检测
参与运算的两个数中,只要有一个是0就直接得出结果。因为浮点数运算过程比较复杂
对阶操作
小数点需要对齐,当阶码大小不相等时,需要先对阶,通过移动尾数改变阶码大小,尾数向右移一位,阶码值加一,反之减一。移动尾数过程中可能存在部分二进制被移出。但如果向左移动,会使高位移出,误差更大,因此规定在对阶时,选择阶码小的数进行操作
尾数求和
对接完成后,按位相加即可,如9.8*10^38 + 6.5*10^37 = 9.8*10^38 + 0.65*10^38 = 10.45^38
结果格式化
求和完毕后,如果整数为不在[1, 9],则需要左右调整,尾数向右移动称为右规,向左移动称为左规。10.45^38要调整为1.045^39
结果舍入
因为对接或右规时,尾数需要移动,移出的位会被丢弃,为了减少精度损失,需要先将移出的这部分数据保存出来,称为保护位,格式化后再根据保护位进行舍入处理。
示例
1.0 - 0.9
1.0 - 0.9 = 1.0 + (-0.9)
1.0的浮点数标识
二进制表示
1.0: [0011 1111 1000 0000 0000 0000 0000 0000]
-0.9: [1011 1111 0110 0110 0110 0110 0110 0110]
对阶
1.0的阶码是127,-0.9的阶码是126,比较阶码后需要向右移动-0.9位数的补码,使其变成127,最高位补1。
-0.9的尾数位补码为:[0001 1001 1001 1001 1001 1010]
对阶后为:[1000 1100 1100 1100 1100 1101]
尾数求和
尾数转换成补码相加
[1000 1100 1100 1100 1100 1101]
+[1000 0000 0000 0000 0000 0000]
=[0000 1100 1100 1100 1100 1101]
规格化
尾数的最高位必须是1,所以需要将结果向左移动4位,阶码减4。
符号位为1
移动后阶码等于123(二进制[1111011])
尾数为[1100 1100 1100 1100 1101 0000]
隐藏最高位后是[100 1100 1100 1100 1101 0000]
最终1.0-0.9二进制表示[1011 1101 1100 1100 1100 1100 1101 0000]
尾数小数点后对应的十进制是 (1 + 2^-1 + 2^-4 + 2^-5 + 2^-8 + 2^-9 + 2^-12 + 2^-13 + 2^-16 + 2^-17 + 2^-19) * 2^-4 = 0.100000024
所以通过上面的实例分析,我们了解了 为什么浮点数1.0-0.9 结果为 0.100000024
总结
二进制和浮点数是我们最初接触计算机时学习的基础理论,日常开发中也会经常遇到,但很容易忽视。下面总结下我学完本章内容收获到有意思的知识点:
- 反码和补码诞生的原因:计算机中只有加法器,没有减法器。通过原码相加,结果会出错,所以出现了反码,又因为反码存在+0和-0问题,所以出现了补码。
- 判断两个数符号是否相同:可以通过使用 ((b >> 31) ^ (a >> 31)) == 0 来判断两个整数正负是否相同
- 浮点数分单精度和双精度:单精度浮点数占四个字节32位,从左到右,1位符号位,8位阶码位(指数的移码),23位尾数位
- 浮点数加减运算:0值检测 -> 对阶 -> 尾数求和 -> 规格化 -> 结果舍入
更多文章
见我的博客:https://nc2era.com
written by AloofJr,转载请注明出处