3.11.1 浮点传送和转换操作

3.11.1 浮点传送和转换操作

图 3-46 给出了一组在内存和 XMM 寄存器之间以及从一个 XMM 寄存器到另一个不做任何转换的传送浮点数的指令。引用内存的指令是标量指令,意味着它们只对单个而不是一组封装好的数据值进行操作。数据要么保存在内存中(由表中的 M32 和 M64 指明),要么保存在 XMM 寄存器中(在表中用 X 表示)。无论数据对齐与否,这些指令都能正确执行,不过代码优化规则建议 32 位内存数据满足 4 字节对齐,64 位数据满足 8 字节对齐。内存引用的指定方式与整数 MOV 指令的一样,包括偏移量、基址寄存器、变址寄存器和伸缩因子的所有可能的组合。

指令 目的 描述
vmovss M32 X 传送单精度数
vmovss X M32 传送单精度数
vmovsd M64 X 传送双精度数
vmovsd X M64 传送双精度数
vmovaps X X 传送对齐的封装好的单精度数
vmovapd X X 传送对齐的封装好的双精度数

图 3-46 浮点传送指令。这些操作在内存和寄存器之间以及一对寄存器之间传送值(X:XMM 寄存器(例如 %xmm3);M32:32 位内存范围;M64:64 位内存范围)

GCC 只用标量传送操作从内存传送数据到 XMM 寄存器或从 XMM 寄存器传送数据到内存。对于在两个 XMM 寄存器之间传送数据,GCC 会使用两种指令之一,即用 vmovaps 传送单精度数,用 vmovapd 传送双精度数。对于这些情况,程序复制整个寄存器还是只复制低位值既不会影响程序功能,也不会影响执行速度,所以使用这些指令还是针对标量数据的指令没有实质上的差别。指令名字中的字母“a”表示“aligned(对齐的)”。当用于读写内存时,如果地址不满足 16 字节对齐,它们会导致异常。在两个寄存器之间传送数据,绝不会出现错误对齐的状况。

下面是一个不同浮点传送操作的例子,考虑以下 C 函数

float float_mov(float v1, float *src, float *dst) {
    float v2 = *src;
    *dst = v1;
    return v2;
}

与它相关联的 x86-64 汇编代码为

float float_mov(float v1, float *src, float *dst)
v1 in %xmm0, src in %rdi, dst in %rsi

1  float_mov:
2      vmovaps %xmm0, %xmm1       Copy v1
3      vmovss  (%rdi), %xmm0      Read v2 from src
4      vmovss  %xmm1, (%rsi)      Write v1 to dst
5      ret                        Return v2 in %xmm0

这个例子中可以看到它使用了 vmovaps 指令把数据从一个寄存器复制到另一个,使用了 vmovss 指令把数据从内存复制到 XMM 寄存器以及从 XMM 寄存器复制到内存。

图 3-47 和图 3-48 给出了在浮点数和整数数据类型之间以及不同浮点格式之间进行转换的指令集合。这些都是对单个数据值进行操作的标量指令。图 3-47 中的指令把一个从 XMM 寄存器或内存中读出的浮点值进行转换,并将结果写入一个通用寄存器(例如 %rax%ebx 等)。把浮点值转换成整数时,指令会执行截断(truncation),把值向 0 进行舍入,这是 C 和大多数其他编程语言的要求。

指令 目的 描述
vcvttss2si X/M32 R32 用截断的方法把单精度数转换成整数
vcvttsd2si X/M64 R32 用截断的方法把双精度数转换成整数
vcvttss2siq X/M32 R64 用截断的方法把单精度数转换成四字整数
vcvttsd2siq X/M64 R64 用截断的方法把双精度数转换成四字整数

图 3-47 双操作数浮点转换指令。这些操作将浮点数转换成整数(X:XMM 寄存器(例如 %xmm3);R32:32 位通用寄存器(例如 %eax);R64:64 位通用寄存器(例如 %rax);M32:32 位内存范围;M64:64 位内存范围)

指令 源 1 源 2 目的 描述
vcvtsi2ss M32/R32 X X 把整数转换成单精度数
vcvtsi2sd M32/R32 X X 把整数转换成双精度数
vcvtsi2ssq M64/R64 X X 把四字整数转换成单精度数
vcvtsi2sdq M64/R64 X X 把四字整数转换成双精度数

图 3-48 三操作数浮点转换指令。这些操作将第一个源的数据类型转换成目的的数据类型。第二个源值对结果的低位字节没有影响(X:XMM 寄存器(例如 %xmm3);M32:32 位内存范围;M64:64 位内存范围)

图 3-48 中的指令把整数转换成浮点数。它们使用的是不太常见的三操作数格式,有两个源和一个目的。第一个操作数读自于内存或一个通用目的寄存器。这里可以忽略第二个操作数,因为它的值只会影响结果的高位字节。而我们的目标必须是 XMM 寄存器。在最常见的使用场景中,第二个源和目的操作数都是一样的,就像下面这条指令:

vcvtsi2sdq %rax, %xmm1, %xmm1

这条指令从寄存器 %rax 读出一个长整数,把它转换成数据类型 double,并把结果存放进 XMM 寄存器 %xmm1 的低字节中。

最后,要在两种不同的浮点格式之间转换,GCC 的当前版本生成的代码需要单独说明。假设 %xmm0 的低位 4 字节保存着一个单精度值,很容易就想到用下面这条指令

vcvtss2sd %xmm0, %xmm0, %xmm0

把它转换成一个双精度值,并将结果存储在寄存器 %xmm0 的低 8 字节。不过我们发现 GCC 生成的代码如下

   Conversion from single to double precision
1  vunpcklps %xmm0, %xmm0, %xmm0    Replicate first vector element
2  vcvtps2pd  %xmm0, %xmm0           Convert two vector elements to double

vunpcklps 指令通常用来交叉放置来自两个 XMM 寄存器的值,把它们存储到第三个寄存器中。也就是说,如果一个源寄存器的内容为字 [s₃, s₂, s₁, s₀],另一个源寄存器为字 [d₃, d₂, d₁, d₀],那么目的寄存器的值会是 [s₁, d₁, s₀, d₀]。在上面的代码中,我们看到三个操作数使用同一个寄存器,所以如果原始寄存器的值为 [x₃, x₂, x₁, x₀],那么该指令会将寄存器的值更新为值 [x₁, x₁, x₀, x₀]。vcvtps2pd 指令把源 XMM 寄存器中的两个低位单精度值扩展成目的 XMM 寄存器中的两个双精度值。对前面 vunpcklps 指令的结果应用这条指令会得到值 [dx₀, dx₀],这里 dx₀ 是将 x₀ 转换成双精度后的结果。即,这两条指令的最终效果是将原始的 %xmm0 低位 4 字节中的单精度值转换成双精度值,再将其两个副本保存到 %xmm0 中。我们不太清楚 GCC 为什么会生成这样的代码,这样做既没有好处,也没有必要在 XMM 寄存器中把这个值复制一遍。

对于把双精度转换为单精度,GCC 会产生类似的代码:

   Conversion from double to single precision
1  vmovddup    %xmm0, %xmm0    Replicate first vector element
2  vcvtpd2psx  %xmm0, %xmm0    Convert two vector elements to single

假设这些指令开始执行前寄存器 %xmm0 保存着两个双精度值 [x₁, x₀]。然后 vmovddup 指令把它设置为 [x₀, x₀]。vcvtpd2psx 指令把这两个值转换成单精度,再存放到该寄存器的低位一半中,并将高位一半设置为 0,得到结果 [0.0, 0.0, x₀, x₀](回想一下,浮点值 0.0 是由位模式全 0 表示的)。同样,用这种方式把一种精度转换成另一种精度,而不用下面的单条指令,没有明显直接的意义:

vcvtsd2ss %xmm0, %xmm0, %xmm0

下面是一个不同浮点转换操作的例子,考虑以下 C 函数

double fcvt(int i, float *fp, double *dp, long *lp)
{
    float f = *fp; double d = *dp; long l = *lp;
    *lp = (long) d;
    *fp = (float) i;
    *dp = (double) l;
    return (double) f;
}

以及它对应的 x86-64 汇编代码

double fcvt(int i, float *fp, double *dp, long *lp)
i in %edi, fp in %rsi, dp in %rdx, lp in %rcx

 1  fcvt:
 2      vmovss      (%rsi), %xmm0              Get f = *fp
 3      movq        (%rcx), %rax               Get l = *lp
 4      vcvttsd2siq (%rdx), %r8                Get d = *dp and convert to long
 5      movq        %r8, (%rcx)                Store at lp
 6      vcvtsi2ss   %edi, %xmm1, %xmm1         Convert i to float
 7      vmovss      %xmm1, (%rsi)              Store at fp
 8      vcvtsi2sdq  %rax, %xmm1, %xmm1         Convert l to double
 9      vmovsd      %xmm1, (%rdx)              Store at dp
        The following two instructions convert f to double
10      vunpcklps   %xmm0, %xmm0, %xmm0
11      vcvtps2pd   %xmm0, %xmm0
12      ret                                     Return f

fcvt 的所有参数都是通过通用寄存器传递的,因为它们不是整数就是指针。结果通过寄存器 %xmm0 返回。如图 3-45 中描述的,这是 floatdouble 值指定的返回寄存器。在这段代码中,可以看到图 3-46~图 3-48 中的许多传送和转换指令,还可以看到 GCC 将单精度转换为双精度的方法。

练习题 3.50 对于下面的 C 代码,表达式 val1val4 分别对应程序值 ifdl

double fcvt2(int *ip, float *fp, double *dp, long l)
{
    int i = *ip; float f = *fp; double d = *dp;
    *ip = (int) val1;
    *fp = (float) val2;
    *dp = (double) val3;
    return (double) val4;
}

根据该函数如下的 x86-64 代码,确定这个映射关系:

double fcvt2(int *ip, float *fp, double *dp, long l)
ip in %rdi, fp in %rsi, dp in %rdx, l in %rcx
Result returned in %xmm0

 1  fcvt2:
 2      movl        (%rdi), %eax
 3      vmovss      (%rsi), %xmm0
 4      vcvttsd2si  (%rdx), %r8d
 5      movl        %r8d, (%rdi)
 6      vcvtsi2ss   %eax, %xmm1, %xmm1
 7      vmovss      %xmm1, (%rsi)
 8      vcvtsi2sdq  %rcx, %xmm1, %xmm1
 9      vmovsd      %xmm1, (%rdx)
10      vunpcklps   %xmm0, %xmm0, %xmm0
11      vcvtps2pd   %xmm0, %xmm0
12      ret

练习题 3.51 下面的 C 函数将类型为 src_t 的参数转换为类型为 dest_t 的返回值,这里两种数据类型都用 typedef 定义:

dest_t cvt(src_t x)
{
    dest_t y = (dest_t) x;
    return y;
}

在 x86-64 上执行这段代码,假设参数 x%xmm0 中,或者在寄存器 %rdi 的某个适当的命名部分中(即 %rdi%edi)。用一条或两条指令来完成类型转换,并把结果值复制到寄存器 %rax 的某个适当命名部分中(整数结果),或 %xmm0 中(浮点结果)。给出这条或这些指令,包括源和目的寄存器。

Tx Ty 指令
long double vcvtsi2sdq %rdi, %xmm0
double int
double float
long float
float long