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。像 ja 和 jb 这样的指令可以根据标志位的各种组合进行条件跳转。
来看一个浮点比较的例子,图 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 版本。