一、浮点数
浮点表示对形如V=x*2y的有理数进行编码,对执行涉及非常大的数字、非常接近0的数字和作为实数运算的近似值的计算,是很有用的。
1.1 二进制小数
十进制表示法使用如下形式的表示:dmdm-1…d0.d-1…dn。其中每个十进制数di的取值范围是0 ~ 9。数字权的定义与十进制小数点符号相关,这意味着小数点左边的数字的权是10的正幂,得到整数值,而小数点右边的数字的权是10的负幂,得到小数值。
例如,12.3410表示数字1 * 101+2 * 100+3 * 10-1+4 * 10-2=12 34 100 \frac{34}{100} 10034。
类似的,二进制表示法每个二进制数字(位),小数点左边的数字的权是2的正幂,得到整数值,而小数点右边的数字的权是2的负幂,得到小数值。
例如,1011.12表示数字1 * 23+0 * 22+1 * 21+1 * 20+1 * 2-1=11 1 2 \frac{1}{2} 21。
在仅考虑有限长度编码的情况下,十进制表示法不能准确的表示像 1 3 \frac{1}{3} 31和 5 7 \frac{5}{7} 75这样的数。类似的,小数的二进制表示法只能表示那些能够被写成x*2y的数,其它的值只能被近似地表示。
1.2 IEEE浮点表示
定点表示法不能有效地表示非常大的数字。例如,表达式5*2100是用101后面跟随100个零的位模式来表示。IEEE浮点标准通过给定x和y的值,来表示形如x * 2y的数。
IEEE浮点标准用V=(-1)s * M * 2E 的形式来表示一个数。根据s、M、E将浮点数的位表示划分为三个字段,分别对这些值进行编码。
- 符号s决定该数是负数还是正数,由一个单独的符号位s直接编码。
- 尾数M是一个二进制小数,由n位小数字段frac编码。
- 阶码E的作用是对浮点数加权,这个权重是2的E次幂,由k位的阶码字段exp编码。
在单精度浮点格式(C语言中的float)中,s、exp、frac字段分别为1、8、23;在双精度浮点格式(C语言中的double)中分别为1、11、52。
根据exp的值,被编码的值可以分成三种不同的情况。
(1)规格化的值:exp不全为0,也不全为1。
阶段字段被解释为以偏置形式表示的有符号整数,阶码的值为E=e-Bias。其中e是阶段位向量(ek-1…e1e0)表示的无符号数(1 ~ 254;1 ~ 2046),而Bias是值为2k-1-1的偏置值(127;1023)。两者之差的结果为指数的取值范围。对于单精度浮点来说是-126 ~ 127,而对于双精度浮点来说是-1022 ~ 1023。
小数字段frac被解释为描述小数值f,f的范围为[0,1],二进制表示为0.fn-1…f1f0。而尾数M定义为1+f,所以M为1.fn-1…f1f0。使用这种方法获得一个额外的精度位,既然第一位总是等于1,就不需要显式地表示。
(2)非规格化的值:exp全为0
此时,阶码值是E=1-Bias,而尾数的值是M=f,也就是小数字段的值,不包含隐含的开头的1。
非规格化数有两个用途:
1、表示数值0。规格化的M总是大于等于1,因此不能用来表示0。符号位不同得到的+0和-0在某些方面是不同的。
2、表示非常接近于0.0的数。
(3)特殊值:exp全为1
此时,阶码全为1。
1、当小数域全为0时,得到的值表示无穷,s=0时表示正无穷,s=1时表示负无穷。无穷能表示溢出的结果,例如,两个非常大的数相乘或者除以零时。
2、当小数域为非零时,结果值被称为NaN,即不是一个数(Not a Number)的缩写。一些运算的结果不能是实数或无穷时返回NaN值。例如,极端
−
1
2
\sqrt[2]{-1}
2−1和
∞
\infty
∞-
∞
\infty
∞。
1.3 C语言中的浮点数
所有的C语言版本提供了两种不同的浮点数据类型:float和double。在支持IEEE浮点格式的机器上,这些数据类型对应于单精度和双精度浮点。C语言标准不要求机器使用IEEE浮点,所以没有标准的方法改变舍入方式,或者得到-0、+ ∞ \infty ∞、- ∞ \infty ∞或者NaN之类的特殊值。
当在int、float和double格式之间进行强制转换时,程序改变数值和位模式的原则如下:
- 从int转换为float,数字不会溢出,但是可能被舍入。
- 从int或float转换为double,能够保留精确的数值。
- 从double转换为float,值可能会溢出或者舍入。
- 从float或者double转换为int,值将会向零舍入。
二、浮点代码
2.1 浮点体系结构
MMX技术,即MultiMedia eXtensions(多媒体拓展),在CPU中加入特地为视频信号、音频信号以及图像处理而设计的57条指令。这些指令的本意是允许多个操作以并行模式执行,称为单指令多数据或SIMD,在这种模式中,对多个不同的数据并行执行同一个操作。从MMX发展到SSE(Streaming SIMD Extension),以及最新的AVX(Advanced Vector Extension)。每个版本的拓展都是管理寄存器组的数据,这些寄存器组在MMX中称为“MM”寄存器,64位;SSE中称为“XMM”寄存器,128位;而在AVX中称为“YMM”寄存器,256位。每个寄存器都可以存放整数或浮点数。
AVX浮点体系结构允许数据存储在16个YMM寄存器中,名字为%ymm0 ~ %ymm15,每个YMM寄存器都是256位,当对标量数据操作时,这些寄存器只保存浮点数,而且只使用低32位(float)或64位(double)。汇编代码用寄存器的SSE XMM寄存器名字%ymm0 ~ %ymm15来引用,每个XMM寄存器都对应的YMM寄存器的低128位。
2.2 浮点传送和转换操作
(1)不做转换的浮点数传送指令,包括从内存传送数据到XMM寄存器vmovss
,或从XMM寄存器传送数据到内存vmovsd
,以及在两个XMM寄存器之间传送数据vmovaps、vmovapd
。
(2)把一个从XMM寄存器或内存中读出的浮点值进行转换,并将结果写入一个通用寄存器。把浮点值转换为整数时,指令会执行截断,把值向0进行舍入。
(3)整数转换成浮点数,使用三操作数格式,包括两个源和一个目的。目标必须是XMM寄存器。
2.3 过程中的浮点代码
当函数包含指针、整数和浮点数混合的参数时,指针和整数通过通用寄存器传递,而浮点数通过XMM寄存器传递。
- XMM寄存器%xmm0~%xmm7最多可以传递8个浮点参数,按照参数列表中的顺序使用这些寄存器,额外的浮点参数通过栈传递。
- 函数使用寄存器%xmm0来返回浮点值。
- 所有的XMM寄存器都是调用者保存的,被调用者可以不用保存就覆盖这些寄存器中的任意一个。
2.4 浮点运算操作
每条指令有一个或两个源操作数,一个目的操作数,第一个源操作数S1可以是一个XMM寄存器或一个内存位置。第二个源操作数和目的操作数都必须是XMM寄存器,每个操作都有一条针对单精度的指令和一条针对双精度的指令,结果存放在目的寄存器中。
2.5 定义和使用浮点常数
和整数运算操作不同,AVX浮点操作不能以立即数值作为操作数。相反,编译器必须为所有的常量值分配和初始化存储空间。然后,代码把这些值从内存读入。以摄氏度到华氏度转换的函数为例说明该过程。
double cel2fahr(double temp) {
return 1.8 * temp + 32.0;
}
相应的x86-64汇编代码部分如下:
可以看到,函数从标号为.LC2的内存位置读出1.8,从标号为.LC3的位置读入值32.0。.LC2有两个值:3435973837(0xcccc cccd)和1073532108(0x3ffc cccc)。机器采用的是小端字节顺序,第一个给出的是低位4字节,第二个给出的是高位4字节。所以,符号位和指数字段为0x3ff(1023),减去偏置值(1023)得到指数0,小数字段为0xc cccc cccc cccd,二进制小数表示0.8,加上隐式的1得到常量1.8。
2.6 位级操作
这些操作都作用于封装好的数据,即更新整个目的XMM寄存器,对两个源寄存器的所有位都实施指定的位级操作。
2.7 比较操作
类似于CMP指令,浮点比较指令比较操作数,并且设置条件码指示两者的相对值。参数S2必须在XMM寄存器中,而S1可以在XMM寄存器或内存中。