【从入门到放弃-Java】码出高效-计算机基础-二进制和浮点数-阿里云开发者社区

开发者社区> Java技术进阶> 正文
登录阅读全文

【从入门到放弃-Java】码出高效-计算机基础-二进制和浮点数

简介: - Java - 码出高效 - 浮点数 - 二进制

前言

最近在学习孤尽大神内网分享的资料时,被安利了一本书【码出高效】,这也是他在【阿里巴巴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,转载请注明出处

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
Java技术进阶
使用钉钉扫一扫加入圈子
+ 订阅

Java技术进阶成长,课程资料,案例解析,实战经验全都有!

官网链接