3.11.6 浮点比较操作

3.11.6 浮点比较操作

AVX2 提供了两条用于比较浮点数值的指令:

指令 基于 描述
ucomiss S₁, S₂ S₂ - S₁ 比较单精度值
ucomisd S₁, S₂ S₂ - S₁ 比较双精度值

这些指令类似于 CMP 指令(参见 3.6 节),它们都比较操作数 S₁ 和 S₂(但是顺序可能与预计的相反),并且设置条件码指示它们的相对值。与 cmpq 一样,它们遵循以相反顺序列出操作数的 ATT 格式惯例。参数 S₂ 必须在 XMM 寄存器中,而 S₁ 可以在 XMM 寄存器中,也可以在内存中。

浮点比较指令会设置三个条件码:零标志位 ZF、进位标志位 CF 和奇偶标志位 PF。3.6.1 节中我们没有讲奇偶标志位,因为它在 GCC 产生的 x86 代码中不太常见。对于整数操作,当最近的一次算术或逻辑运算产生的值的最低位字节是偶校验的(即这个字节中有偶数个 1),那么就会设置这个标志位。不过对于浮点比较,当两个操作数中任一个是 NaN 时,会设置该位。根据惯例,C 语言中如果有个参数为 NaN,就认为比较失败了,这个标志位就被用来发现这样的条件。例如,当 x 为 NaN 时,比较 x==x 都会得到 0。

条件码的设置条件如下:

顺序 S₂:S₁ CF ZF PF
无序的 1 1 1
S₂ < S₁ 1 0 0
S₂ = S₁ 0 1 0
S₂ > S₁ 0 0 0

当任一操作数为 NaN 时,就会出现无序的情况。可以通过奇偶标志位发现这种情况。通常 jp(jump on parity)指令是条件跳转,条件就是浮点比较得到一个无序的结果。除了这种情况以外,进位和零标志位的值都和对应的无符号比较一样:当两个操作数相等时,设置 ZF;当 S₂ < S₁ 时,设置 CF。像 jajb 这样的指令可以根据标志位的各种组合进行条件跳转。

来看一个浮点比较的例子,图 3-51a 中的 C 函数会根据参数 x 与 0.0 的相对关系进行分类,返回一个枚举类型作为结果。C 中的枚举类型是编码为整数的,所以函数可能的值为:0(NEG)、1(ZERO)、2(POS)和 3(OTHER)。当 x 的值为 NaN 时,会出现最后一种结果。

typedef enum {NEG, ZERO, POS, OTHER} range_t;

range_t find_range(float x)
{
    int result;
    if (x < 0)
        result = NEG;
    else if (x == 0)
        result = ZERO;
    else if (x > 0)
        result = POS;
    else
        result = OTHER;
    return result;
}

a)C 代码

range_t find_range(float x)
x in %xmm0

 1  find_range:
 2      vxorps   %xmm1, %xmm1, %xmm1       Set %xmm1 = 0
 3      vucomiss %xmm0, %xmm1               Compare 0:x
 4      ja       .L5                        If >, goto neg
 5      vucomiss %xmm1, %xmm0               Compare x:0
 6      jp       .L8                        If NaN, goto posornan
 7      movl     $1, %eax                   result = ZERO
 8      je       .L3                        If =, goto done
 9  .L8:                                    posornan:
10      vucomiss .LC0(%rip), %xmm0          Compare x:0
11      setbe    %al                        Set result = NaN ? 1 : 0
12      movzbl   %al, %eax                  Zero-extend
13      addl     $2, %eax                   result += 2 (POS for > 0, OTHER for NaN)
14      ret                                 Return
15  .L5:                                    neg:
16      movl     $0, %eax                   result = NEG
17  .L3:                                    done:
18      rep; ret                            Return

b)产生的汇编代码

图 3-51 浮点代码中的条件分支说明

GCC 为 find_range 生成图 3-51b 中的代码。这段代码的效率不是很高:它比较了 x 和 0.0 三次,即使一次比较就能获得所需的信息。它还生成了浮点常数两次:一次使用 vxorps,另一次从内存读出这个值。让我们追踪这个函数,看看四种可能的比较结果:

x < 0.0 第 4 行的 ja 分支指令会选择跳转,跳转到结尾,返回值为 0。

x = 0.0 ja(第 4 行)和 jp(第 6 行)两个分支语句都会选择不跳转,但是 je 分支(第 8 行)会选择跳转,以 %eax 等于 1 返回。

x > 0.0 这三个分支都不会选择跳转。setbe(第 11 行)会得到 0,addl 指令(第 13 行)会把它增加,得到返回值 2。

x = NaN jp 分支(第 6 行)会选择跳转。第三个 vucomiss 指令(第 10 行)会设置进位和零标志位,因此 setbe 指令(第 11 行)和后面的指令会把 %eax 设置为 1。addl 指令(第 13 行)会把它增加,得到返回值 3。

家庭作业 3.73 和 3.74 中,你需要试着手动生成 find_range 更高效的实现。

练习题 3.57 函数 funct3 有如下原型:

double funct3(int *ap, double b, long c, float *dp);

对于此函数,GCC 产生如下代码:

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

 1  funct3:
 2      vmovss      (%rdx), %xmm1
 3      vcvtsi2sd   (%rdi), %xmm2, %xmm2
 4      vucomisd    %xmm2, %xmm0
 5      jbe         .L8
 6      vcvtsi2ssq  %rsi, %xmm0, %xmm0
 7      vmulss      %xmm1, %xmm0, %xmm1
 8      vunpcklps   %xmm1, %xmm1, %xmm1
 9      vcvtps2pd   %xmm1, %xmm0
10      ret
11  .L8:
12      vaddss      %xmm1, %xmm1, %xmm1
13      vcvtsi2ssq  %rsi, %xmm0, %xmm0
14      vaddss      %xmm1, %xmm0, %xmm0
15      vunpcklps   %xmm0, %xmm0, %xmm0
16      vcvtps2pd   %xmm0, %xmm0
17      ret

写出 funct3 的 C 版本。