欢迎大家来我的博客做客: re1ife.top
浮点数的表示
- 浮点数的规格化表示是唯一的:小数点前只有一个非零数
只要对尾数和指数分别编码,就可以表示一个浮点数(实数)。
32位浮点数格式的规格化数表示范围:
+/- 0.1xxxx * 2^E
- 第0位:符号位
- 第1-8位:表示阶码,偏置常数 128,
- 第9-31位:小数的尾数,规格化尾数的小数点后一位总是1(二进制下),因此可以省去,用23为表示24个数字
- 最大正数: 0.1111…1 * 211…1 = (1-2-24) * 2 127
- 0.1 = 2-1
-
- 00000 - 0.000…1 = 0.11…1
- 偏置常数 + 127 = 255 = 28 - 1
- 最小正数: 0.100…0 * 200…0 = (2-1 )* 2-128 = 2-129
- 为什么是0.100…0?
- 因为小数点之后的1 是被省略的1
- 最大正数: 0.1111…1 * 211…1 = (1-2-24) * 2 127
综上所述:
机器0: 尾数全部为0的时候,或者落在下溢区域的数。
浮点数比定点数表示的范围更大,但是数的个数没有变多,因此更为稀疏了。
浮点数的规格化
现在所有通用计算机使用 IEEE-754表示浮点数
- 规格化数的形式: +/- 1.xxxx * R^Exponent
-
单精度浮点数表示格式:
-
Exponent(阶码 / 指数编码):
- SP规格化数阶码范围为0000 0001 (-126) ~ 1111 1110 (127)
- 全1 或 全0 用来表示特殊值
- bias为 127 (single), 1023 (double)
- 全部为0时可以看作 -127, 注意此时的偏置常数为127
- SP规格化数阶码范围为0000 0001 (-126) ~ 1111 1110 (127)
-
Significand(尾数):
- 规格化尾数最高位总是1,所以隐含表示,省1位
所以单精度表示的真正的值为: (-1)S x (1 + Significand) x 2Exponent-127
例题:已知单精度浮点数-12.75 ,求在机器中表示的数.
- 负数,因此 S为1
- 12 = 8 + 4 = 1100B
- 0.75 计算方法:
- 0.75 * 2 = 1.50 进一
- 0.50 * 2 = 1.00 进一
- 得到 11B
- 12.75 = 1100.11B = 1.10011 x 23
- 127 + 3 = 1000 0010 B
- 最终结果:
- 省去了小数点前面的 1
特殊数字表示
- 0 的表示
- -0 : 1 0000 0000 0000 0000 0000 0000 000
- +0:0 0000 0000 0000 0000 0000 0000 000
- +/- ♾️
-
- 0 11111111 00000000000000000000000
-
- :1 11111111 00000000000000000000000
- 浮点数 / 0 得到的值是♾️ inf
-
- 非数: Not a Number(NaN)
- Exponent 全一, Singnificand非0
- 例如 sqrt(-4.0)
非规格化数的表示
由上图的上半部分可以看到,2-126 ~ 2-125 中的刻度要比 2-125到2-124更密集。为啥?
[!INFO]
指数位的值加1,相当于将浮点数乘以2,而指数位的值减1,相当于将浮点数除以2。因此,在指
数位相同的情况下,相邻的两个浮点数之间的差值是2的幂次方。对于2-126到2-125中的每个浮点数,它们的指数位都为-126,因此它们之间的差值是2的-23次幂,即2-126乘以2的-23次幂,约为1.18 x 10-38。而对于2-125到2-124中的每个浮点数,它们的指数位都为-125,因此它们之间的差值是2的-23次幂乘以2,即2-125乘以2的-23次幂,约为2.35 x 10-38。因此,2-126到2-125中的刻度比2-125到2-124更密集。
而 GAP
区域则是下溢区域, 为了更好利用这部分数字,我门可以使用非规格化表示。
其他数据的编码
- 逻辑字符
逻辑值只有:真或假。
逻辑值的表示用二进制的一位来表示,0 为假 1为真。
- 西文字符编码
ASCII码来表示。
字符串的操作: 传送和比较。
- 汉字及国际字符编码表示
特点: 表意文字、是个方块图形、数量巨大
编码形式:
- 输入 ——输入码: 对汉字用相应按键编码
- 查找传输处理——内码用于在系统中存储、查找、
- 打印显示——字模点阵或轮廓描述
数据宽度与存储
数据的基本宽度
在计算机中最小的处理、存储、传输信息的最小单位是——比特(bit)
二进制信息最基本的计量单位是“字节”(Byte)
- 现代计算机存储器按字节编址
- 字节是最小可寻址单位
- LSB: 小端序 MSB:大端序
在实际使用中,我们也用字来作为单位:
在IA-32
中 字: 16位 、 字长:32位
字是一中体系结构如 IA-32
规定长度, 字长是数据通路的宽度 ,也就是寄存器、数据传输…的宽度。
- 数据通路:CPU内部数据流经的路径以及路径上的部件,主要是CPU内部进行数据运算、存储和传送的部件,这些部件的宽度基本上要一致,才能相互匹配
- 字:表示被处理信息的单位,用来度量数据类型的宽度。
[!INFO] x86体系结构定义“字”的宽度为16位,但从386开始字长就是32位
数据量的度量单位
储存二进制信息是度量单位要远大于字节或字。
- KB 千字节 1KB = 210字节 = 1024B
- MB 兆字节 1MB = 220字节 = 1024KB
- GB 千兆字节 1GB = 230字节 = 1024MB
- …
速率单位:
注意是bit, 我们常说的MB/s,是千字节每秒
- “千比特/秒”(kb/s),1kbps=103 b/s=1000 bps
- “兆比特/秒”(Mb/s),1Mbps=106 b/s =1000 kbps
- “千兆比特/秒”(Gb/s),1Gbps=109 b/s =1000 Mbps
- “兆兆比特/秒”(Tb/s),1Tbps=1012 b/s =1000 Gbps
程序中的数据类型宽度
- C语言中的数值类型宽度
Compaq Alpha 是一个针对高端应用的64位机器,字长64
数据的存储和排列方式
在计算机中,一个基本数据可能占用多个存储单元。如 int x = 10
x的存放地址为100, 机器数为FFFFFFF6H 占用四个单元。
那么在计算机中给出存放地址一般是最小地址,即x
总共占用 100~103 这个四个单元。那么这四个单元内数据的存放顺序就是我们要说的大端序/小端序。
左侧即位小端序,右侧为大端序。
大端序符合人的阅读顺序,小端序则是逆序。
大端序(Big Endian): MSB 所在的地址是数的地址
小端序(Little Endian): LSB 所在的地址是数的地址
C语言中 union
存放顺序是所有成员是从低地址开始。
#include <stdio.h>
int main(){
union NUM{
int a;
char b;
} num;
num.a = 0x12345678;
if(num.b == 0x12)
printf("Big Endian \n");
else
printf("Little Endian \n");
printf("num.b = 0x%X\n", num.b);
return 0;
}
字节交换问题
如果一个是大端序、一个是小端序在交换数值是就会发生一些小问题。
数据的表示和运算
按位运算和逻辑运算
按位运算: 对位串实现“掩码”(mask) 操作或相应的其他处理。(主要用于多媒体数据或状态\控制信息处理)。
-
主要操作:
- 按位或:“|”
- 按位与:“&”
- 按位取反:“~”
- 按位异或:“^”
-
逻辑运算: 用于关系表达式的运算。
- 或:||
- 与:&
- 非:!
与按位运算不同的是,逻辑运算符针对的是一个整体。结果类型是了逻辑值。
移位运算与位截断/扩展运算
移位运算:
移位运算主要用于扩大和缩小数值的2、4、8…倍。
- 操作:
- 左移: x<<k;
- 右移: x>>k;
在进行移位运算是保持着
- 无符号数:高(低)位移出,低(高)位补0
- 有符号数:
- 左移:高(低)位移出,低(高)位补0
- 右移:低位移出,高位补符,可能发生有效数据丢失
的规则, 也就是说有可能会导致溢出或数据丢失。
位截断/扩展运算
C语言中没有专门操作运算符,但是在类型转换是会实际发生。
例如在这一段代码中:
int i = 32768;
short si = (short) i;
int j = si;
i
的所占单元要大于short类型,因此在转化为 short时,si等于的是i被截断的值:-32768。
j
作为 int 类型,从short转化为 int 会发生扩展运算,但是值实际没有发生变化, j = -32768
变化过程:
- i = 32768 00 00 80 00
- si = -32768 80 00
- j = -32768 FF FF 80 00
因此从上面我们可以发现截断与扩展的规则:
- 截断: 强行将高位丢弃,故可能发生“溢出”
- 扩展:
- 无符号数:0扩展,前面补0
- 带符号整数:符号扩展,前面补符
算术运算
讲完了基本的运算规则,那么计算机如何是实现高级语言中的运算呢?
在前面中,我们学习了补码。
那么根据补码的定义:
[ X ] 补 = 2 n + X ( − 2 n − 1 ≤ X ≤ 2 n − 1 , m o d 2 n ) {[X]}_补 = 2^n + X (-{2}^{n-1} \leq X \le 2^{n-1}, mod 2^n) [X]补=2n+X(−2n−1≤X≤2n−1,mod2n)
我们可以发现有如下公式:
[!INFO]
[x+y]补=2n+x+y= 2n+x+2n+y= [x]补+[y]补 (mod 2n)
[x-y]补=2n+x-y= 2n+x+2n-y= [x]补+[-y]补 (mod 2n)
[-x]补 = [x] ‾ \overline{\text{[x]}} [x]补 + 1
上面的第三个式子就是实现减法的主要工作。
利用带标志加法器,可构造整数加/减运算器,进行以下运算:
- 无符号整数加、无符号整数减
- 带符号整数加、带符号整数减
这也是为什么我们说,减法是由加法来实现的。
一位加法器——全加器
如图一位加法器又叫全加器。符号为FA (Fulling Adder)
两个加数为A与B, 低位进位为 Cin ,和为 F ,向高位进位Cout.
运算逻辑表达式:
F
=
A
⊕
B
⊕
C
i
n
{F = A \oplus B \oplus Cin }
F=A⊕B⊕Cin
C
o
u
t
=
A
∧
B
+
A
∧
C
i
n
+
B
∧
C
i
n
{Cout = A \land B + A \land Cin + B \land Cin }
Cout=A∧B+A∧Cin+B∧Cin
n位加法器
n位全加器可以通过使用 n 个全加器实现。
让我们来欣赏一下加法器的美图吧,家人们:
我们将 Cout 即全加器的高位进位进行串联,作为下一个全加器的 Cin
是一个逻辑门电路。
如下图连接方式:
其他所有算术运算部件基于加法器。
图上的基本加法器无法用于两个 n 位带整数(补码) 加减。因为无法判断是否溢出。
- N位带标志加法器
在编程之中,我们需要经常比较大小, 原理是我们通过底层的加法器做减法得到的标志信息判断的。
下面就是N位带标志加法器的果图,十分的SEXY:
- 标志含义:
- OF:溢出标志
- O F = C m ⊕ C m − 1 {OF = C_m \oplus C_{m-1}} OF=Cm⊕Cm−1
- SF: 符号标志
- S F = F n − 1 {SF = F_{n-1}} SF=Fn−1
- ZF: 0 标志
- ZF = 1 当且仅当 F = 0即结果为0 时
- CF:进位\借位标志
- C F = C o u t ⊕ C i n {CF = Cout \oplus Cin} CF=Cout⊕Cin
- OF:溢出标志
那么我们学习了n位带标志的加法器,我们是如何计算的呢?
在前文中,提到了补码的规则。负数的补码是其正数补码尾数+1的值。
因此在计算 A - B 时,实际上计算的是:
[A-B]补=2n+A-B= 2n+A+2n-B= [A]补+[-B]补 (mod 2n)
通过多路选择器(Mux),来选择B输出的是原码还是补码。
当 Sub 为-1,就是做减法,因此我们要使用补码。反之亦然。
那么我们在上面的基础上添加寄存器、移位器以及逻辑控制就可以实现ALU、乘/除以及浮点运算电路。
- ALU
可以进行基本的算术运算与逻辑运算,核心电路是带标志位加法器。
- 如何判断结果输出的是针对的哪一种运算?
- 是看标志ALUop,它的位数决定操作种类。当她有三位时,就有 8 种操作。
整数的加减运算
在计算机中,指针、地址等通常为无符号整数,因此在进行相关操作是要进行无符号整数加减。
无符号整数与带符号整数加减运算电路完全一样, 称之为整数加减运算部件。基于带标志加法器。
[!INFO]
- 计算机所有运算基于加法器
- 加法器不知道运算是带符号数还是不带符号数
- 加法器不判断对错,总是取低n为作为结果,生成标志信息
- 条件标志位:在运算电路中产生,被记录到专门的寄存器。
- 被称为 程序/状态字寄存器或标志寄存器
- 被称为 程序/状态字寄存器或标志寄存器
在做加法时判断是否溢出:
- 无符号整数: CF = 1
- 带符号整数: OF = 1
在做加法时判断是否溢出:
- 无符号整数: CF = 1
- 带符号整数: OF = 1
利用减法判断两数大小:
- 无符号整数: CF = 0 (减法借位标识),则 A大于B
- 有符号整数 OF = SF (溢出标志 = 符号标志)的时候,A大于B
减法
n是代表是几位数字。
无符号减公式:(小于零是要模,因为溢出了)
r e s u l t = { x − y ( x − y > 0 ) x − y + 2 n ( x − y < 0 ) result = \left\{ \begin{aligned} x - y &&(x-y>0) \\ x - y + 2^n && (x - y < 0) \end{aligned} \right. result={x−yx−y+2n(x−y>0)(x−y<0)
带符号减公式:
r e s u l t = { x − y − 2 n ( 2 n − 1 ≤ x − y ) 正溢出 x − y ( − 2 n − 1 ≤ x − y < 2 n − 1 ) 正常 x − y + 2 n ( x − y < − 2 n − 1 ) 负溢出 result = \left\{ \begin{aligned} x - y - 2^n &&(2^{n-1} \le x-y) && 正溢出\\ x - y &&(-2^{n-1} \le x-y \lt 2^{n-1}) && 正常\\ x - y + 2^n && (x - y < -2^{n-1}) && 负溢出 \end{aligned} \right. result=⎩ ⎨ ⎧x−y−2nx−yx−y+2n(2n−1≤x−y)(−2n−1≤x−y<2n−1)(x−y<−2n−1)正溢出正常负溢出
加法
无符号加公式:
r e s u l t = { x + y ( x + y ≤ 2 n ) x − y + 2 n ( 2 n ≤ x + y < 2 n + 1 ) result = \left\{ \begin{aligned} x + y &&(x+y \le 2^n) \\ x - y + 2^n && (2^{n} \le x + y \lt 2^{n+1}) \end{aligned} \right. result={x+yx−y+2n(x+y≤2n)(2n≤x+y<2n+1)
带符号减公式:
r e s u l t = { x + y − 2 n ( 2 n − 1 ≤ x + y ) 正溢出 x + y ( − 2 n − 1 ≤ x + y < 2 n − 1 ) 正常 x + y + 2 n ( x + y < − 2 n − 1 ) 负溢出 result = \left\{ \begin{aligned} x + y - 2^n &&(2^{n-1} \le x+y) && 正溢出\\ x + y &&(-2^{n-1} \le x+y \lt 2^{n-1}) && 正常\\ x + y + 2^n && (x + y < -2^{n-1}) && 负溢出 \end{aligned} \right. result=⎩ ⎨ ⎧x+y−2nx+yx+y+2n(2n−1≤x+y)(−2n−1≤x+y<2n−1)(x+y<−2n−1)正溢出正常负溢出
整数乘除法
整数乘法
高级语言中, 两个n位数乘积的会得到 2n位 的数字,但是我们只取低n位。
在计算机内部,x2带符号整数不一定是正数(溢出成负值),但是浮点数一定是正数。
例如,四位的带符号整数,x = 5,会发生溢出。 计算过程:
由于是两个4位带符号整数相乘会得到一个8位的整数,我们取低四位即 1001
他的真值就是 -111
,因此就是 -7。
假定两个n位无符号整数xu
和yu
对应的机器数为Xu
和Yu
,pu
=xu
×yu
,pu
为n位无符号整数且对应的机器数为Pu
; 两个n位带符号整数xs
和ys
对应的机器数为Xs
和Ys
,ps
=xs
×ys
,ps
为n位带符号整数且对应的机器数为Ps
。 若Xu
=Xs
且Yu
=Ys
,则Pu
=Ps
。
-溢出判断: - 无符号:若Puh=0,则不溢出 - 带符号:若Psh每位都等于Ps的最高位,则不溢出
通过X*Y的高n位可以用来判断溢出 无符号:若高n位全0,则不溢出,否则溢出 带符号:若高n位全0或全1且等于低n位的最高位,则不溢出。
整数除法
手算二进制除法:
其实与一般除法无异。
在带符号的整数,n 位整数除n位整数,除 -2n-1 / - 1 = 2n-1 发生溢出。
商的绝对值不可能比被除数的绝对值更大。
整数除法其商也是整数,随意在无法整除是要舍入。
通常下,是按照0方向舍入。
整数除0的结果无法用机器数表示, 会出现异常。
我们举个例子:
int a = 0x80000000
int b = a / -1
printf("%d\n",b)
运行结果是: -2147483648。
通过 Objdump反汇编可以得知, -1 被优化为了取负指令neg
,因此没有发生除法溢出。
但是,结果是错误的,0x80000000 是 -231 , 结果应该是 231 . 但是由于32位的补码无法表示,因此溢出。
int a = 0x80000000;
int b = -1;
int c = a / b;
printf("%d\n",c)
结果是 Float Point Exception. 其实就是编译器底层无法优化为取反指令。
变量与常数之间的运算
计算机中除法运算更为复杂,因此不能使用流水线方式,一次除法需要消耗30多个或者更多始终周期。
为了缩短时间,编译器在处理一个变量与一个2的幂次形式的整除时,经常才去右移运算实现。
在可以整除的时候可以直接右移。
- 无符号:逻辑右移
- 12/4 = 3 = 0000 1100 >> 2 = 0000 0011
- 有符号:算术右移
- -12/4 = 3 = 1111 0100 >> 2 = 1111 1101
不能整除时,右移移出的位中有非0,需要处理。
- 不能整除时,采用朝0舍入,即截断处理。
- 无符号数、带符号正整数:移出低位之后直接丢弃。
- 举例:14 /4 = 3 = 0000 1110 >> 2 = 0000 0011
- 带符号负整数:加偏移量(2k - 1) 然后右移k位,低位截断。k是右移位数。
- ❎直接截断: -14 / 4 = -4 = 1111 0010 >> 2 = 1111 1100 = -4
- 纠偏右移: k = 2 (-14 + 22 - 1) / 4= -3 = 1111 0101 >> 2 = 1111 11101 = -3
- 无符号数、带符号正整数:移出低位之后直接丢弃。
浮点数运算及结果
<<<<<<< HEAD
可能会有几种情况:
- 阶码上溢:一个正指数超过最大允许值 => +∞/-∞ /溢出
- 阶码下溢:一个负指数超过最小允许值 => +0/-0
- 尾数溢出: 最高有效位有进位 => 右规
- 1.5 + 1.5 = 11.00…000 但是可以右移将非规格化数合规
- 非规格化位数:数值部分高位为0 => 左规
- 1.5 - 1.0 = 0.10…0 尾数要左移,阶码减1
- 右规或对阶
IEEE规定的五种异常情况
- 无效运算
- 运算时有一个是非有限数 (♾️)
- 结果无效: NaN 。。。。
- 除以0 (无限大)
- 数太大(阶上溢)
- 数太小(阶下溢)
- 结果不精确如 1 / 3 无法精确表示
浮点数目的运算过程可以大致分为一下几个过程:
1.求阶差: ∆e=Ye – Xe (若Ye > Xe,则结果的阶码为Ye)
2. 对阶: 将两个小数的阶码对齐,小阶向大阶对齐
- 举个例子: 1.123 x 105 + 2.230 x 103 = 1.123 x 105 + 0.02230 x 105
IEEE 754尾数右移时,要将隐含的“1”移到小数部分,高位补0,移出的低位保留到特定的“附加位”上
方便我们来计算。
3. 尾数加减: Xm*2Xe-Ye ± Ym
4. 规格化:
- 当尾数高位为0,则需左规:尾数左移一次,阶码减1,直到MSB为1或阶码为00000000(-126,非规格化数)
- 每次阶码减1后要判断阶码是否下溢(比最小可表示的阶码还要小)
-
当尾数最高位有进位,需右规:尾数右移一次,阶码加1,直到MSB为1
- 每次阶码加1后要判断阶码是否上溢(比最大可表示的阶码还要大)
阶码溢出异常处理:阶码上溢,则结果溢出;阶码下溢到无法用非规格化数表示,则结果为0
5. 若尾数比规定位数长(有附加位),则考虑舍入
6. 若尾数结果为0, 结果也应该为0
附加位
为了保证每次运算尽可能不丢失精度,我们所想出来的办法是——附加位。
IEEE 754: 规定中间结果必须在右边加两个附加位。
- Guard 保护位:在Significand右边的位
- Round 舍入位:在保护位右边的位
作用:用以保护对阶时右移的位或运算的中间结果。
处理: ①左规时被移到significand中; ② 作为舍入的依据。
举例子:
默认舍入方式:就近舍入。
Z1和Z2分别是结果Z的最近的可表示的左、右两个数 )
- 就近舍入(精度最高):舍入为最近可表示的数
非中间值:0舍1入;
中间值:强迫结果为偶数-慢
举例:
附加位为
01:舍
11:入
10:(强迫结果为偶数)
2. 朝+∞方向舍入:舍入为Z2(正向舍入)
3. 朝-∞方向舍入:舍入为Z1(负向舍入)
4. 朝0方向舍入:截去。正数:取Z1; 负数:取Z2
- C语言中float和double分别对应IEEE 754单精度浮点数格式和双精度浮点数
- long double类型的长度和格式随编译器和处理器类型的不同而有所不同,IA-32中是80位扩展精度格式
- 从int转换为float时,不会发生溢出,但可能有数据被舍入
- 从int或 float转换为double时,因为double的有效位数更多,故能保留精确值
- double转换为float和int时,可能发生溢出,此外,由于有效位数变少,故可能被舍入
- float 或double转换为int时,因为int没有小数部分,所以数据可能会向0方向被截断
范围与精度
float型表示范围:
最大数据: +1.11…1 x 2127 约为 +3.4x1038
大数吃小数
这个是精度问题所导致的:
在程序之中,我们总是先计算括号部分,因此在第二行的中,1.0在对阶中就被省略成0了。