浮点运算指令
程序员文章站
2022-05-28 12:09:29
...
上一节介绍了浮点数与各种数值类型之间的相互转换所用到的指令,本节则介绍进行各种浮点运算所需的指令。
先来看看下面一组用于执行算术运算的标量 AVX2 浮点指令。
其中,每条指令有一个或两个源操作数 S1、S2,以及一个目的操作数 D。第一个源操作数 S1 可以是一个 XMM 寄存器或一个内存位置,而第二个源操作数和目的操作数都必须是 XMM 寄存器。每个操作都有一条针对单精度和一条双精度的指令。结果都存放在目的寄存器中。
考虑下面的浮点 C 函数。
其对应的 x86-64 代码类似如下。
不同于整数操作,AVX 浮点操作不能以立即数值作为操作数。相反,编译器必须为所有的常量值分配和初始化存储空间,然后再把这些值从内存读入。下面这个从摄氏度到华氏度转换的函数说明了这个问题。
在采用小端字节顺序的机器上生成的 x86-64 汇编代码类似如下。
可以看到,值 1.8 是从标号为“.LC2”内存位置处读出,值 32.0 是从标号“.LC3”内存位置处读出。而这些标号对应的值,每一个都是通过一对“.long”声明和十进制表示的值指定的。要理解这些数所表示的浮点值,需要知道浮点数在系统底层的表示方法,具体可以参考《深入理解计算机系统》第二章——信息的表示和处理,碍于篇幅限制,这里只给出结论。以标号“.LC2”的两个数值为例,因为机器采用的是小端字节顺序,所以第一个值 3435973837(0xcccccccd)给出的是低位 4 字节,第二个值 1073532108(0x3ffccccc)给出的是高位 4 字节。从高位字节抽取指数字段为 0x3ff(1023),减去偏置值 1023 得到指数 0,将两个值的小数位连接起来,得到小数字段 0xccccccccccccd,二进制小数表示为 0.8,加上隐含的 1 就得到 1.8。
除了进行普通运算操作,GCC 生成的代码还可以在 XMM 寄存器上执行位级操作。下图所示的指令会对一个 XMM 寄存器中的所有 128 位进行位操作,不过同前面一样,我们一般只关心目的寄存器的低 4 或 8 字节。
另外,AVX2 还提供了两条用于比较浮点数值的指令。
这里的操作数 S2 必须在 XMM 寄存器中,而 S1 还可以在内存中。这两条指令类似于 CMP 指令,也是通过设置条件码来指示它们的相对值。浮点比较指令会设置三个条件码:零标志位 ZF、进位标志位 CF 和奇偶标志位 PF。PF 标志位在 GCC 产生的 x86 代码中不太常见,对于整数操作,当最近的一次算术或逻辑运算产生的值的最低位字节是偶校验的(即这个字节中有偶数个 1),那么就会设置这个标志位。而对于浮点数,当两个操作数中任一个是 NaN 时,就会设置该位。这个标志位就是用来发现是否有参数为 NaN 的情况,因为当 x 为 NaN 时,比较 x==x 都会得到 0,表示比较失败。
条件码的设置条件如下。
当任一操作数为 NaN 时,就会出现图中无序的情况。通常 jp(jump on parity) 指令是条件跳转,条件就是浮点比较得到一个无序的结果。进位和零标志位的使用同整数的一样,这里不再赘述。
参考书籍:
1、《深入理解计算机系统》第三章——程序的机器级表示。
先来看看下面一组用于执行算术运算的标量 AVX2 浮点指令。
其中,每条指令有一个或两个源操作数 S1、S2,以及一个目的操作数 D。第一个源操作数 S1 可以是一个 XMM 寄存器或一个内存位置,而第二个源操作数和目的操作数都必须是 XMM 寄存器。每个操作都有一条针对单精度和一条双精度的指令。结果都存放在目的寄存器中。
考虑下面的浮点 C 函数。
double funct(double a, float x, double b, int i){ return a*x - b/i; }
其对应的 x86-64 代码类似如下。
; double funct(double a, float x, double b, int i); ; a in %xmm0, x in %xmm1, b in %xmm2, i in %edi funct: ; The following two instructions convert x to double vunpcklps %xmm1, %xmm1, %xmm1 vcvtps2pd %xmm1, %xmm1 vmulsd %xmm0, %xmm1, %xmm0 ; Multiply a by x vcvtsi2sd %edi, %xmm1, %xmm1 ; Convert i to double vdivsd %xmm1, %xmm2, %xmm2 ; Compute b/i vsubsd %xmm2, %xmm0, %xmm0 ; Subtract from a*x ret ; Return
不同于整数操作,AVX 浮点操作不能以立即数值作为操作数。相反,编译器必须为所有的常量值分配和初始化存储空间,然后再把这些值从内存读入。下面这个从摄氏度到华氏度转换的函数说明了这个问题。
double cel2fahr(double temp){ return 1.8 * temp + 32.0; }
在采用小端字节顺序的机器上生成的 x86-64 汇编代码类似如下。
; double cel2fahr(double temp) ; temp in %xmm0 cel2fahr: vmulsd .LC2(%rip), %xmm0, %xmm0 ; Multiply by 1.8 vaddsd .LC3(%rip), %xmm0, %xmm0 ; Add 32.0 ret .LC2: .long 3435973837 ; Low-order 4 bytes of 1.8 .long 1073532108 ; High-order 4 bytes of 1.8 .LC3: .long 0 ; Low-order 4 bytes of 32.0 .long 1077936128 ; High-order 4 bytes of 32.0
可以看到,值 1.8 是从标号为“.LC2”内存位置处读出,值 32.0 是从标号“.LC3”内存位置处读出。而这些标号对应的值,每一个都是通过一对“.long”声明和十进制表示的值指定的。要理解这些数所表示的浮点值,需要知道浮点数在系统底层的表示方法,具体可以参考《深入理解计算机系统》第二章——信息的表示和处理,碍于篇幅限制,这里只给出结论。以标号“.LC2”的两个数值为例,因为机器采用的是小端字节顺序,所以第一个值 3435973837(0xcccccccd)给出的是低位 4 字节,第二个值 1073532108(0x3ffccccc)给出的是高位 4 字节。从高位字节抽取指数字段为 0x3ff(1023),减去偏置值 1023 得到指数 0,将两个值的小数位连接起来,得到小数字段 0xccccccccccccd,二进制小数表示为 0.8,加上隐含的 1 就得到 1.8。
除了进行普通运算操作,GCC 生成的代码还可以在 XMM 寄存器上执行位级操作。下图所示的指令会对一个 XMM 寄存器中的所有 128 位进行位操作,不过同前面一样,我们一般只关心目的寄存器的低 4 或 8 字节。
另外,AVX2 还提供了两条用于比较浮点数值的指令。
这里的操作数 S2 必须在 XMM 寄存器中,而 S1 还可以在内存中。这两条指令类似于 CMP 指令,也是通过设置条件码来指示它们的相对值。浮点比较指令会设置三个条件码:零标志位 ZF、进位标志位 CF 和奇偶标志位 PF。PF 标志位在 GCC 产生的 x86 代码中不太常见,对于整数操作,当最近的一次算术或逻辑运算产生的值的最低位字节是偶校验的(即这个字节中有偶数个 1),那么就会设置这个标志位。而对于浮点数,当两个操作数中任一个是 NaN 时,就会设置该位。这个标志位就是用来发现是否有参数为 NaN 的情况,因为当 x 为 NaN 时,比较 x==x 都会得到 0,表示比较失败。
条件码的设置条件如下。
当任一操作数为 NaN 时,就会出现图中无序的情况。通常 jp(jump on parity) 指令是条件跳转,条件就是浮点比较得到一个无序的结果。进位和零标志位的使用同整数的一样,这里不再赘述。
参考书籍:
1、《深入理解计算机系统》第三章——程序的机器级表示。
上一篇: redis 客户端实现
下一篇: 浮点寄存器概述