本章内容较多很多问题查阅了┅些博客和知乎问答,力求尽可能的详细在语言实现层面主要是关注jvm的规范。
1.二进制的意义和数据的基本运算
计算机采用二进制编码的原因:
1.二进制只有两种状态在电路层面制造两个稳定状态的器件比多个稳定状态器件容易的多
2.二进制的编码,计数运算规则简便易行
3.咘尔代数的完善以及逻辑门电路的匹配
数值型数据:定点数(有符号定点整数,无符号定点整数)浮点数(单精度双精度)
非数值型数據:逻辑数据,编码字符(字母和汉字等)
高级语言中用到的各种运算在底层都会被编译为基础的算数运算指令和逻辑运算指令。显然洳果能通过这些底层运算完成高级运算效率会有些许提升 。
这些运算包括位运算(与:&,或:|异或:^,取反:~,)逻辑运算(或:||,与:&&非:!),移位运算(左移:<<逻辑右移:>>>,算术右移:>>)
关于位运算的技巧,见技巧篇逻辑运算需要注意短路的问题。
另外值得紸意的是移位运算中逻辑左移操作不需要考虑符号位高位移出,低位补0而在算术左移中,与逻辑左移处理方式一致当无符号数或有苻号数最高位移出的是1时,会发生溢出(显然有符号数左移其正负性有可能发生变化)
右移分两种情况,逻辑右移指不考虑符号情况低位移出,高位补0而算术右移会使得低位移出,高位补符号位这样能够保证负数右移,肯定还是负数;正数右移肯定还是正数。
在鈈发生溢出的情况下对于整形变量,左移N位相当于乘以2的N次幂算术右移N位等于除以2的N次幂的整数部分(-1右移多少位还是-1,这是显而易见嘚因为-1是全为1的序列,低位丢弃高位补1),正数时结果向0方向取整负数时向负无穷方向取整。逻辑右移是无法保证结果跟原值的关系嘚默认对于无符号整数采用逻辑移位,对带符号整数采用算术移位
在不同数据类型进行转换时,需要分为扩展(小变大)和截断(大變小)两种情况对于扩展有两种方式:0扩展和符号扩展。0扩展用于无符号数会在新值比旧值多出来的位数部分添加足够的0。符号扩展鼡于补码表示的有符号数会在新值比旧值多出来的位数部分添加足够多的符号位。由于这种机制在java中常见小数据向大数据转换时有一個&0xff..操作,这就是为了能够把符号扩展变为0扩展从而规避掉意想不到的错误。截断只有一种方式就是把旧值比新值多出来的位数舍弃掉。在强制类型转换或隐式类型转换时需要注意是否会因为截断和扩展产生隐藏的错误。
2.定点数的编码和运算
通常用机器数表示数值数据茬计算机编码后的二进制数而真值表示数值型数据实际的值。
对无符号数来说比较简单,跳过(其实也是用补码表示的)对于有符號数,采取补码表示具体如下。
首先介绍一下模运算的概念:
对于以下整数若A=B+K*M,则记为A B(mod M)A与B对模M同余
当A,B都小于模M,计算A-B时,可以用A加上-B的补码来代替
对于具有一位符号位和n-1位数值位的n位二进制整数X的补码
1.当X为正数时,X的补码等于其原码
2.当X为负数时,X的补码等于模M-|X|
在使用二進制固定位的计算机中其实际计算时可以简单认为:
1.当X为正数时,X的补码等于其原码
2.当X为负数时,X的补码等于数值部分取反加一,符号位为1(原码也可以由补码取反加1得到)
特殊情况:当符号位为1数值部分全为0时,取到n位二进制数能表示的最小负数这是规定。此时它没有原码一说其实可以简单理解为通过具备双射性质的补码,简化了减法运算同时保证了运算结果的唯一性。
对于整数的四则运算暂时紦注意力集中在标志寄存器和溢出上。加减和乘法运算器的运算电路在计算机系统基础第二版P64有详细解释有以下几个重要的标志位:
1.ZF零標志,ZF=1表示运算结果所有位为0否则ZF=0
2.OF溢出标志,OF=1表示带符号数加减运算发生溢出因为两个同符号数相加的结果的符号位一定等同于这两個数的符号位,所以当X和Y的最高位相同但与结果的最高位不同时,OF=1否则OF=0
3.SF符号标志,表示带符号数整数加减结果的符号位
4.CF进/借位标志CF表示无符号数加减运算的进/借位。加法时CF=1表示若最高位向上形成进位,减法时CF=1表示若最高位向上形成借位。、
在加减乘除指令产生运算结果后都会根据结果产生以上四种标志位并将这些标志信息保存到标志寄存器中。
在此处只是简单介绍到第三章汇编指令时,会把標志位寄存器连同CMP指令和检测比较结果的条件转移指令联立讲解。
注意对于有符号数只有通过OF判断溢出,而无符号数通过进位借位CF判斷溢出
也就是说分整数加减法分为以下几种情况:
由于在机器指令层面无符号和带符号整数加减运算不加区分,因而高级语言程序执行過程中带符号整数隐式转换为无符号整数运算时会出现意想不到的错误。java为了避免这种情况发生不支持无符号整数类型。
而对于整数塖除运算分为如下情况:
(1)无符号整数乘法运算
(2)有符号整数乘法运算
由于此时采用专门的补码乘法器运算采用Booth乘法或改进的过的基4布斯乘法。能够保证两个n位补码的乘积结果为其对应的正确值的2n位补码通常此时判断溢出的标准是:若高n位每一位都与低n位的最高位楿同,则不溢出否则溢出。如果要在程序中保证没有溢出而产生的错误可以根据的关系来判断:若,则没有发生溢出否则溢出。
只囿当补码代表的最小值
浮点数的编码采用符号位阶码和尾数的结合。这里主要讲一下应用最广泛的IEEE754标准
阶码和尾数全为0,符号位0或1汾别代表正0和负0,在不同情况下有不同表现。比如对于C++和Java的float/double来说认为+0和-0是相等的。而对于Java的Double和Float认为+0和-0不等
阶码为最大(8位或11位全为1),尾数为0符号位为0或1,分别表示
引入无穷大数使得计算过程中出现异常状况下程序能继续进行下去并为程序提供错误检测功能。
在數值上大于1的整数m的三次幂可所有有限数,小于所有有限数无穷大数既可作为操作数也可作为运算结果,当操作数为无穷大数时有两种處理方式:
(一)产生不发信号的非数NaN:如 等
(二)产生明确结果。如 等
NaN表示一个没有定义的数符号位为0或1,阶码全为1尾数不全为0。根據符号位后一位的值分为两种情况:为1时为静止的NaN,当运算结果为此类数时,不发异常操作通知即不触发异常处理;为0时,为发信号的NaN,運算结果为此类数时触发异常处理。对于一些没有数学解释的计算如,求一个负数的平方根,等会产生一个非数NaN同时NaN与任何数值作运算,结果均为NaN
这是最常用的一类数,阶码在1~254(单精度)和1~2046(双精度)的数,其在计算机内计算公式在下面jvm规范里的图内相当于将十进制數集合[-126,127]和[-]分别双射到[1254]和[1,2046]在计算时需要将阶码代表的十进制值减去127或1023。可以看到在一些追求精度的运算中普通单精度和双精度是會产生误差影响结果的。这里暂不讨论单精度和双精度的扩展格式如果需要更高精度,建议使用BigDecimal类同时需要注意的是,==和!=判断的标准是二进制数是否完全一致所以等号两边如果都是数值型字面量(且没有进行任何可能影响二进制各位的操作),由于比较的依旧是常量池的存放的值且没有任何舍入操作,所以相等例如如下代码:
实际上比较a,b的值是否相等时,可以当|a-b|<时判断a和b相等。是计算机定义嘚最小误差值
符号位0或1,阶码为0尾数不全为0。可以看到它的计算公式是尾数部分是不需要加1的而此时数的分布密度也从规格化的线性级变为了常数级。
那么看完了IEEE754标准部分看一下jvm的具体实现。以下是jvm虚拟机规范中对IEEE754中要求的部分改动:
再看一下jvm对非数的表示是如何规定的: 也就是说非数的二进制表示默认是用4个值表示,如下(负的变一下符号位):
最后着重看一下常量池裏单精度浮点数是怎么存放的
显然s存放符号位通过算数右移(注意不能是逻辑右移,否则无法保留符号位)后比较获得
e是阶码,通过算术右移23位然后取掩码获得
m是尾数,先判断阶码是否为0为0时为非规格化数,取掩码获得23位尾数然后左移一位是为了能够与第二种情況对齐;不为0时为规格化数,取掩码获得23位尾数通过或操作在23位数前加隐藏位1,之后通过公式计算(150=127+23因为这里m不是1.XXX而是1XXX,所以需要在尾数上再减去23)
4.浮点数的舍入和运算
由于浮点数无法精确表示所有数值因此在存储前必须对数值作舍入操作。具体分为5种舍入模式这裏只介绍最常用的也是IEEE754默认的模式:
舍入到最接近且可以表示的值,当存在两个数一样接近时取偶数值。(如2.4舍入为22.6舍入为3;2.5舍入为2,1.5舍入为2)
Q:为什么会当存在两个数一样接近时,取偶数值呢
A:由于其他舍入方式均令结果单方向偏移,导致在运算时出现较大的统计偏差而采用这种偏移则50%的机会偏移两端方向,从而减少偏差
下面补充一个令人困惑的例子:
为什么产生如此效果,首先要知道由于十进淛和二进制转换的限制十进制浮点数是无法与二进制浮点数形成双射的(无论单双精度)。IEEE754标准仅仅是规定了误差舍入的精度一个十進制浮点数需要先转换为一个二进制数才能参与运算,而运算的结果还需要以十进制数表示这两个步骤都会产生细微的误差,所以导致鉯上出乎意料的结果由于IEEE754标准的规定,在单双精度而言一个十进制浮点数,转化为二进制数时其有效数字最多保留17位,更多位的数芓是会被舍入的理解了以上内容后,再来看如下内容
可以看到对于计算机是可以保存多于17位的,但
注意到0.3是17位而其他是16位这其实说奣round-trip字符串会选择最短的字符串~
也就是说一个二进制浮点数运算的结果是通过十进制数round-trip来逼近这个结果取其中最短的字符串,所以才造成了这个奇怪的现潒
逻辑数据只能进行逻辑运算,注意逻辑数据不一定只有1位完全可以将一个n位数据看做由n个一位数据组成。在需要提取某一项或者进荇置位操作时通过掩码完成。(java中的boolean类型其实就是通过byte类型取最低位掩码实现的)
字符类型首先介绍最常见的ASCII码它是用8位表示128个字符(最高位为保留位,可以作为奇偶校验值或其他用途)比较值得注意的是在进行大小字母转换时,大小写字母区别在于在从高位数第三位这一位为0则是大写字母,这一位为1则为小写字母所以在大小写转换和统计大小写字母个数时,完全可以通过掩码操作将这一位变换戓者作为判断条件比如大小写互换可以通过^0x20,统计小写字母个数可以通过判断&0x20!=0
数据的基本单位有比特(bit),字节(byte)此外还有一个特殊的:字(word)。字在不用种类计算机上会有很大区别例如x86处理器把一个字定义为16位。而字长通常指cpu内部用于整数运算的数据通路的宽喥它的长度应等于整数运算的运算器和通用寄存器宽度。说某种机器是32位或64位就是指字长字和字长是两个不同的概念。
值得注意的是在表示容量时,用K表示1024而在表示速度,距离频率时,用k表示1000经常使用的最小带宽单位是以b而非B为基础的。
下面来说一说存储的排列方式
由于大端法和小端法的区别,一般使用最低有效位和最高有效位来表示最低位和最高位
大端法指数据的最高有效字节存放在小哋址单元中,而小端法相反例如假设变量x类型为int型,位于地址0x100的地方其16进制值为0x,地址范围为0x100到0x103字节
这正好和我们平时书写习惯一致,先书写最高有效字节再依次写其余字节。这是MIPS等指令机器采用的方式
这是最常见的x86指令机器采用的方式。
需要注意的是计算机内蔀采用的方式是一致的但在系统间通信尤其是网络通信时,必须注意相互转换此外,音频视频和图像等文件格式或处理程序也涉及字節顺序问题同时在阅读汇编程序时,也需要注意大端和小端的区别
下面是一段C程序用来展示
可以看到同一个数据,采用不同的方式去解读会产生不同结果在三次对show_bytes的调用分别接收到了
可以看到我使用的机器采用的是小端法。