1. 浮点寄存器
  2. 浮点数指令 – 传送和转换操作
  3. 浮点数指令 – 寄存器分配
  4. 浮点数指令 – 浮点数的运算和浮点常数
  5. 浮点数指令 – 浮点数的位级操作
  6. 浮点数指令 – 浮点数比较

浮点寄存器

AVX2的浮点寄存器一共有16个, 命名比较规则, 从%YMM0 – %YMM15, 每个寄存器都是256位长. 为什么这么长, 是因为指令集可以操作标量浮点数, 也可以操作向量浮点数.

操作标量浮点数的时候, 这些寄存器只保存浮点数, 而且只使用低32位或者低64位, 对应的称为%XMM0 – &XMM15寄存器.

浮点数指令 – 传送和转换操作

操作浮点数有着特殊的指令, 先来看看纯粹的传送指令:

  1. vmovss, 从寄存器和内存之间互相传送一个单精度数
  2. vmovsd, 从寄存器和内存之间互相传送一个双精度数
  3. vomvaps, 在寄存器之间互相传送对齐的单精度数
  4. vomvapd, 在寄存器之间传送对齐的双精度数

前两个指令只能用在寄存器和内存互相传送, 最后两个指令用于寄存器间互相传送, a表示对齐.

然后是转换指令. 之前的整型数据, 改变数据类型只是给编译器所用, 整型数据的底层表示不会改变. 但在浮点数中, 将整型和浮点数互相转换, 是会发生变化的.

先是浮点数转换成整数的操作, 就像之前手工操作的一样, 是采取先计算好对应的幂 ,然后舍入到最靠近0的数来将浮点数转换成整数

  1. vcvttss2si X/M32 R32, 单精度转32位整数
  2. vcvttsd2si X/M64 R32, 双精度转32位整数
  3. vcvttss2siq X/M32 R64, 单精度转64位整数
  4. vcvttsd2siq X/M64 R64, 双精度转64位整数

这四个指令的第一个操作数可以是Xmm寄存器或者其他通用寄存器, 第二个操作数也就是目标寄存器必须是通用整型寄存器

把整数转换成浮点则是三操作数指令:

  1. vcvtsi2ss M32/R32 X X, 把32位整数转换为单精度浮点
  2. vcvtsi2sd M32/R32 X X, 把32位整数转换为双精度浮点
  3. vcvtsi2ssq M64/R64 X X, 把64位整数转换为单精度浮点
  4. vcvtsi2sdq M64/R64 X X, 把64位整数转换为双精度浮点

这其中的第二个操作数影响结果的高位字节, 但不影响低位字节, 所以对于浮点数转换没有任何影响, 一般也设置成和目标寄存器一样的寄存器.

可以发现, 相比于整数需要关心传送的长度和所使用的寄存器, 浮点数的传送指令不需要关心所使用的寄存器, 统一都使用Xmm系列寄存器, 只需要根据长度来操作即可.

而浮点数的互相转换, 单精度数在GCC里会使用将XMM的内容排列成一个向量再扩充为双精度的方法, 完成之后XMM的低64位就是所需的双精度数.

双精度数也是向量操作, 会先把高位和低位都换成和低位一样的双精度数, 然后同时转换成单精度, 把两个单精度都放在低64位中, 并把高位清零.

可以看到浮点寄存器相比整型寄存器, 一些向量操作很有意思.

练习 3.50 确定代码中的对应关系

有如下函数:

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;
}

和对应的汇编代码, 确认 val1-val4 是哪个变量:

double fcvt2(int *ip, float *fp, double *dp, long l)
ip in %rdi, fp in %rsi, dp in %rdx, l in %rcx
fcvt2:
    movl        (%rdi), %eax        把i的值放入%eax
    vmovss      (%rsi), %xmm0       传送单精度数, 把f 放入%xmm0
    vcvttsd2si  (%rdx), %r8d        这个指令是双精度转32位整数, 放入 %r8d, 注意, 转换成int的只有 val1, 所以val1 就是 d
    movl        %r8d, (%rdi)        将转换后的int值写入到ip对应的内存位置, 结合上一句, 很显然 val1 确实就是 d
    vcvtsi2ss   %eax, %xmm1, %xmm1  转换32位整数为单精度浮点, %eax中是i, 结合程序代码看, 转换为float的只有 val2, 也就是 i
    vmovss      %xmm1, (%rsi)       将转换后的单精度数写入fp对应的地址, 显然 val2 = i
    vcvtsi2sdq  %rcx, %xmm1, %xmm1  这是把64位整数转换为双精度浮点, 注意%rcx 中是l,所以这个是转换的l, 要看写入到哪里才能确定
    vmovsd      %xmm1, (%rdx)       把转换后的l写入到dp对应的地址, 所以val3 = l
    vunpcklps   %xmm0, %xmm0, %xmm0 把单精度的f准备转换成双精度
    vcvtps2pd   %xmm0, %xmm0        和上一句一起看, 是将f转换为双精度浮点
    ret                             最后返回, 则很显然, val4 = f

映射关系是:

  1. val1 = d
  2. val2 = i
  3. val3 = l
  4. val4 = f

练习 3.51 写出转换指令

有如下程序:

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

根据x和y的类型, 写出对应代码, 如果x是整数, 就存放在%rdi中, 如果是浮点数 , 就存放在%xmm0中. 返回值如果是整数, 就存放在%rax系列中, 如果是浮点数, 就存放在%xmm0中.

x的类型 y的类型 指令
long double vcvtsi2sdq %rdi, %xmm0, %xmm0
double int vcvttsd2si %xmm0, %eax
double float vcvtsd2ss %xmm0, %xmm0, %xmm0
long float vcvtsi2ssq %rdi, %xmm0, %xmm0
float long vcvttss2siq %xmm0, %rax

浮点数指令 – 寄存器分配

在过程中使用到浮点数的时候, 之前浮点寄存器的时候可以看到, %xmm0 – %xmm7可以传送8个浮点参数, 和整型参数一样, 超过的部分可以通过栈来传递.

%xmm0同时也是返回浮点数的寄存器.

所有的浮点寄存器都是由调用者保存的, 也就是说过程可以任意操作所有浮点寄存器, 无需恢复状态.

如果参数是整型和浮点混合, 实际上就是单独提取出所有的整型和浮点参数, 再分别按照寄存器顺序排列.

练习 3.52 确定参数的寄存器分配

  1. double g1(double a, long b, float c, int d), 参数分别为浮点, 整型, 浮点, 整型, 寄存器排布为
    a in %xmm0, b in %rdi, c in %xmm1, d in %esi
  2. double g2(int a, double *b, float *c, long d), 四个参数都是整型或者指针, 所以全部排布在整型寄存器中:
    a in %edi, b in %rsi, c in %rdx, d in %rcx
  3. double g3(double *a, double b, int c, float d), 四个参数是指针, 浮点, int, 浮点, 寄存器排布为:
    a in %rdi, b in %xmm0, c in %esi, d in %xmm1
  4. double g3(float a, int *b, float c, double d), 四个参数是浮点, 指针, 浮点, 浮点, 寄存器排布为:
    a in %xmm0, b in %rdi, c in %xmm1, d in %xmm2

浮点数指令 – 浮点数的运算和浮点常数

浮点数的运算也有加减乘除等标量运算, 根据运算的内容不同, 每个指令有一个或二个源操作数S1, S2, 一个目标操作数D. 第一个源操作数可以是内存或者寄存器, 第二个源操作数和目标操作数必须是寄存器.

单精度 双精度 效果 描述
vaddss vaddsd 三操作数, S1+S2的结果存放到D 浮点数相加
vsubss vsubsd 三操作数, S2-S1的结果存放到D中 浮点数减
vmulss vmulsd 三操作数, S2*S1的结果存放到D中 浮点数乘
vdivss vdivsd 三操作数, S2/S1的结果存放到D中 浮点数除
vmaxss vmaxsd 三操作数, 取S2和S1中较大的数放入D中 浮点数取较大数
vminss vminsd 三操作数, 取S2和S1的较小值, 结果放入D中 浮点数取较小数
sqrtss sqrtsd 两操作数, 取S1的平方根放入D中 浮点数开方

在运算的时候, 整数通过整数寄存器传递, 而浮点数通过浮点寄存器传递, 在浮点数和整数进行运算的时候, 会先按照上边的指令将整数转换成浮点数再进行操作. 这就从底层理解了类型转换的原理.

练习 3.53 根据代码确定参数的类型

有如下函数:

double funct1(art1_t p, arg2_t q, arg3_t r, arg4_t s){
    return p/(q+r) - s;
}

对应的汇编代码是:

double funct1(art1_t p, arg2_t q, arg3_t r, arg4_t s)
funct1:
    vcvtsi2ssq      %rsi, %xmm2, %xmm2      把第二个整型寄存器中的四字节整型转换为单精度浮点. 这里从寄存器里能看出来, 使用%rsi, 放入到%xmm2说明有两个整型, 两个浮点
    vaddss          %xmm0, %xmm2, %xmm0     单精度浮点数相加, 这里应该计算的是 q + r , 由此可以判断 %xmm0 是单精度浮点数. 但q和r哪一个是整数转换的, 还不好说
    vcvtsi2ss       %edi, %xmm2, %xmm2      把32位整数转换成单精度浮点, 放在 %xmm2中
    vdivss          %xmm0, %xmm2, %xmm0     用 p/(q+r) 的结果放入%xmm0中, 所以可以知道 p一定是32位整数类型
    vunpcklps       %xmm0, %xmm0, %xmm0
    vcvtps2pd       %xmm0, %xmm0            这两句把p/(q+r)的结果转换成双精度浮点数
    vsubsd          %xmm1, %xmm0, %xmm0     可知%xmm1中的变量是一个双精度浮点数, 可以直接减. 即 s 是双精度浮点数.
    ret

通过分析, 可以知道 p的类型是int, s 是double, 而 q 和 r , 一个是long , 一个是float, 顺序不一定. 但在寄存器中的顺序固定, 第一个参数寄存器是int, 第二个是long.
第一个浮点寄存器是float ,第二个是long.

练习 3.54 根据汇编代码写出C代码

double funct2(double w, int x, float y,long z)
w in %xmm0, x in %edi, y in %xmm1, z in %rsi

funct2:
    vcvtsi2ss       %edi, %xmm2, %xmm2      把 x 转换成 float类型, 可以写成 float temp = (float) x
    vmulss          %xmm1, %xmm2, %xmm1     y = y * temp
    vunpcklps       %xmm1, %xmm1, %xmm1
    vcvtps2pd       %xmm1, %xmm2            把单精度 y 转换为双精度 , 存放在 %xmm2中, double temp2 = (double) y
    vcvtsi2sdq      %rsi, %xmm1, %xmm1      把 z 转换为双精度, 放在 %xmm1中 double temp3 = (double) z
    vdivsd          %xmm1, %xmm0, %xmm0     w = w/temp3
    vsudsd          %xmm0, %xmm2, %xmm0     return  y - w/temp3
    ret

根据汇编代码可以写出C代码如下:

double funct2(double w, int x, float y,long z){
    float temp = (float) x;
    y = y * temp;
    double temp2 = (double) y;
    double temp3 = (double) z;
    w = w/temp3;
    return y - w;
}

去掉所有临时变量, 得到:

double funct2(double w, int x, float y,long z){
    return y * x - w / z;
}

这里所有的浮点指令的操作数不能是常数, 必须先要把常数保存起来, 在使用的时候读取才可以.

看书里的例子:

double cel2fahr(double temp){
    return 1.8 * temp + 32.0;
}

这一个函数, 其中的常量有两个, 一个是1.8, 一个是32.0. 生成的代码如下:

double cel2fahr(double temp)
temp in %xmm0

cel2fahr:
    //这里的%rip是指令地址寄存器, 不能够直接操作
    vmulsd      .LC2(%rip), %xmm0, %xmm0
    vaddsd      .LC3(%rip), %xmm0, %xmm0
    ret
  .LC2
    .long   3435973837
    .long   1073532108

  .LC3
    .long   0
    .long   1077936128

从代码来看, 很显然LC2标号对应的是1.8, 而.LC3对应的常量是32.0. 转换的方法是第一个long对应低位4字节,第二个long对应高位四字节.

将LC2重新排布之后, 可以得到 3f fc cc cc cc cc cc cd, 转换成二进制是:

3    f    f    c    c    c    c    c    c    c    c    c    c    c    c    d
0011 1111 1111 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101

可以看到阶码部分是 011 1111 1111 = 1023, 阶码要减去双精度的偏置值1023, 阶码就是0, 即不用做什么幂变化. 由于这个是规格化数, 有个隐含的1, 实际值是 1.[1100], 就是1.8

练习 3.55 浮点常量是如何表示的

LC3也重新排布一下, 得到 4040000000000000, 二进制是:

4    0    4    0    ......
0100 0000 0100 0000 ......

可以看到阶码部分是 100 0000 0100 = 1028, 减去偏置值 1023 = 5, 由于是规格化数, 有个隐含的1, 所以结果就是 2的5次方的浮点数表示 = 32.0

浮点数指令 – 浮点数的位级操作

浮点代码的位操作会更新寄存器内的全部位. 主要指令有:

单精度 双精度 效果 描述
vxorps xorpd 三操作数, 将 S1 S2 异或的结果放入D 位级异或运算
vandps andpd 三操作数, 将 S1 S2 进行与运算的结果放入D 位级与运算

这些指令一旦执行, 就对所有XMM寄存器里所有的位进行运算. CSAPP这里只关系标量, 所以只取低位, 结果也是一样的.

练习 3.56 根据汇编补全C代码

有如下过程:

double simplefun(double x){
    return EXPR(x);
}

看每个情况的代码是什么意思:

  1.             vmovsd      .LC1(%rip), %xmm1
                vandpd      %xmm1, %xmm0, %xmm0
              .LC2:
                .long 4294967295
                .long 2147483647
                .long 0
                .long 0
    

    第一行指令是传送双精度数, 所以要看一下传送了什么东西: LC2中的值实际上是 00000000 00000000 7fffffff ffffffff. 看低64位的话, 这是01111….
    将x和这串数字进行与运算的结果, 实际上就是将x的最高位符号位置0, 其他位不变, 所以这是一个将浮点数x转换成正数或者说是绝对值的过程.

  2.             vxorpd  %xmm0, %xmm0, %xmm0
    

    这个是将%xmm与自身进行异或运算, 可以知道, 结果是一串0, 所以这个是将寄存器内的浮点数清理成+0

  3.             vmovsd      .LC2(%rip), %xmm1
                vxorpd      %xmm1, %xmm0 ,%xmm0
              .LC2:
                .long 0
                .long -2147483648
                .long 0
                .long 0
    

    这个也需要重新排布LC2看看位级表示: 00000000 00000000 80000000 00000000, 用这个LC2去和 x 进行异或运算, 可以知道 与 0 进行异或, 不改变原来的位. 与1进行异或, 会改变原来的位.
    所以这个函数的意义就是返回将第64位也就是最高位取反的功能, 也就是改变double x 的符号位.

浮点数指令 – 浮点数比较

浮点数比较大小有两条指令:

指令 基于 描述
ucomiss S1, S2 比较S2 : S1 比较单精度值
ucomisd S1, S2 比较 S2 : S1 比较双精度值

这个比较和之前比较整数的写法类似, 实际上是比较第二个操作数减去第一个操作数的结果. 其中S1可以是寄存器也可以是内存, S2必须是浮点寄存器.

比较浮点数的时候所采用的条件码与整型不太一样, 由于IEEE中有NaN的规定, 因此对于浮点数, 任何一方出现NaN的情况, 有一个奇偶标志位PF就会被设置成1. 如果不是1, 则说明S2和S1正常比较, 如果PF为1, 表示无序.

S2 : S1 进位标志CF 零标志位ZF 奇偶标志PF
无序的 1 1 1
S2<S1 1 0 0
S2=S1 0 1 0
S2>S1 0 0 0

除了奇偶标志位之外, 其他的所有判断的情况, 都可以使用无符号比较的条件跳转指令. 判断奇偶标志位的跳转指令是jp.

练习3.57 根据汇编代码补全C代码

double funct3(int *ap, double b, long c, float *dp)
p in %rdi, b in %xmm0, c in %rsi, dp in %rdx

funct3:
    vmovss      (%rdx), %xmm1           传送单精度到%xmm1中, float temp = *dp
    vcvtsi2sd   (%rdi), %xmm2, %xmm2    转换*ap为双精度数, double temp2 = (double) *ap
    vucomisd    %xmm2, %xmm0            比较 b : temp2
    jbe L8                              b<=temp2的时候跳转L8
    vcvtsi2ssq  %rsi, %xmm0, %xmm0      把 c 转换为单精度数 float temp3 = (float) c
    vmulss      %xmm1, %xmm0, %xmm1     temp = temp * temp3
    vunpcklps   %xmm1, %xmm1, %xmm1
    vcvtps2pd   %xmm1, %xmm0            这两句是将temp的结果转换成double返回
    ret

  .L8:
    vaddss      %xmm1, %xmm1, %xmm1     temp = temp * 2
    vcvtsi2ssq  %rsi, %xmm0, %xmm0      float temp3 = (float) c
    vaddss      %xmm1, %xmm0, %xmm0     temp3 = temp3 + temp
    vunpcklps   %xmm1, %xmm1, %xmm1
    vcvtps2pd   %xmm1, %xmm0            这两句是将temp的结果转换成double返回
    ret

根据分析, 写出代码:

double funct3(int *ap, double b, long c, float *dp){
    if(b<=(*ap)){
        return 2 * (*dp) + c;
    } else {
        return (*dp) * c;
    }
}

第三章也看完了, 竟然也全部看懂了. 第四章的流水线看着挺刺激的. 慢慢加油看吧.