【问题标题】:How to implement the totalOrder predicate for floating point values?如何为浮点值实现 totalOrder 谓词?
【发布时间】:2019-12-15 21:44:35
【问题描述】:

IEEE 754 规范在 §5.10 中定义了一个总顺序,我想在汇编中实现它。

the Wikipedia description 看来,这听起来很像可以实现无分支或几乎无分支,但我还没有想出一个像样的方法;而且我在主要编程语言中找不到任何现有的符合规范的实现

比较两个浮点数时,它充当≤操作,除了totalOrder(−0, +0) ∧ ¬ totalOrder(+0, −0),同一个浮点数的不同表示是按其指数乘以符号位排序。然后通过排序 -qNaN

首先检查 NaN,然后​​跳转到浮点比较或处理 NaN 情况是否有意义,或者将浮点值移动到整数寄存器并在那里执行所有操作是否更有意义?

(至少从阅读描述来看,感觉规范作者努力允许使用整数指令直接实现。)

在 x86-64 处理器上实现浮点总阶的“最佳”方法是什么?

【问题讨论】:

  • 我认为您可以只处理符号位,然后进行有符号整数比较。 (将 FP 位模式与整数进行比较只是因为指数偏差而起作用;这就是这样做的原因。除了它们是符号/幅度整数与实现 2 的补码的 x86-64 硬件。) NaN 具有全为指数和非指数- 零有效位因此已经比有限或 +-Inf 更远离0。也相关:SIMD instructions for floating point equality comparison (with NaN == NaN)
  • 例如使用SSE4.2 pcmpgtq,您可以在其普通XMM 寄存器中对double FP 值进行操作,或者对32 位float 的SSE2(x86-64 保证)pcmpgtd 进行操作。 (顺便说一句,您不需要汇编,只需键入双关语到 uint64_t 以使编译器可能使用 movq rax, xmm0,或将内在函数用于向量 regs。)
  • “同一有限值的多个表示”可以应用于 80 位扩展精度“伪非正规”,其中尾数的前导 1 位清零 @987654325 @; x87 FPU 永远不会生成这样的数字,但显然会处理它们。 (float 和 double 隐式存储该位,由 exponent_encoding == 0 暗示)。顺便说一句,你能链接你引用的维基百科文章吗?
  • @PeterCordes:它也适用于十进制交换格式。显示的段落适用于二进制和十进制格式,但我假设 OP 只对二进制格式感兴趣(并且可能只对 binary64 或 binary32 交换格式特别感兴趣)。
  • @PeterCordes 据我了解,总订单会将“负”NaN 作为最小值,将“正”NaN 作为最大值,不确定是否会正确处理......

标签: assembly floating-point x86-64 ieee-754 micro-optimization


【解决方案1】:

如果您将 FP 位模式作为符号/幅度整数进行比较,这一切都行得通,包括 -0 < +0 和 NaN 位模式1。这就是为什么像 binary64 (double) 这样的 IEEE 格式使用有偏指数并将字段按该顺序排列的原因之一。 (另一个是通过++-- 在位模式上方便地实现nextafter。)

这可以通过 2 的补码整数比较有效地实现:

  • 如果符号都被清除:非负数 Just Work
  • 如果只有一个设置了符号位:任何负数都小于任何非负数,包括 -0.0 < +0.00x80000000 < 0x00000000 所以 2 的补码 x <= y Just Works。
  • 如果两者都设置了符号位 ((x&y)>>63):2 的补码 x<y 是符号/幅度 FP x>y。在 x86 asm 中,您可以避免移位而只查看 SF,或者使用 SIMD 元素的高位。

    在不搞乱== 的情况下处理这个问题是很棘手的:你不能只对x&y 进行异或运算@ 登录到< 结果;当他们比较相等时,这会翻转它。当两个输入都为负时,它会给你<=,但在其他情况下会给你<。我不确定这是否可用于排序。


使用SSE4.2 pcmpgtq,您可以在其普通 XMM 寄存器中对双 FP 值进行操作,或者对 SSE2(x86-64 保证)pcmpgtd 进行 32 位浮点运算。 (请注意,pcmpgtqpcmpgtd 相比相对较慢:更少的端口和更高的延迟。https://agner.org/optimize/。例如,在 Skylake 上,1 p5 uop 具有 3c 延迟,而 pcmpgtd 和 pcmpeqq 是 1 uop 用于 p0/p1 和 1周期延迟。)

我们不能只使用一个pcmpgtq + 符号修正来处理按位相等的情况。
x1 bitwise_eq x0 给出的 pcmpgtq 结果为 0,无论输入是正数还是负数。根据总顺序,根据sign(x0&x1) 翻转它会导致不一致的行为,无论您希望 0 还是 1 表示 >>=<<=。但不幸的是,FP 比较的-0.0 == +0.0 行为意味着我们必须对 FP-equal 进行特殊处理,而不仅仅是 FP-unordered。

您不需要汇编,只需在 C 语言中键入 uint64_t,例如让编译器可能使用 movq rax, xmm0,或对向量 reg 使用内部函数。

但如果您使用的是 asm,则可以考虑在 ZF=1 上进行 FP 比较和分支,这将设置为无序或相等,然后才进行整数。如果您希望 NaN 和完全相等(包括+-0.0 == -+0.0)很少见,这可能会很好。请注意,对于the ucomisd docs 中的无序,ZF,CF,PF = 1,1,1。所有 x86 FP 都以相同的方式比较设置标志,直接或通过 fcom/fnstsw ax/lahf

例如,独立版本可能如下所示。 (内联时简化,例如,如果调用者分支,则直接使用jb 而不是setb 分支):

totalOrder:   ; 0/1 integer in EAX = (xmm0 <= xmm1 totalOrder)
    xor      eax, eax
    ucomisd  xmm0, xmm1           ; ZF=0 implies PF=0 (ordered) so just check ZF
    jz    .compare_as_integer     ; unordered or FP-equal
     ; else CF accurately reflects the < or > (total) order of xmm0 vs. xmm1
    setb     al                 ; or branch with jb
    ret

;; SSE4.2, using AVX 3-operand versions.  Use movaps as needed for non-AVX
### Untested
        ; Used for unordered or FP-equal, including -0.0 == +0.0
        ; but also including -1.0 == -1.0 for example
 .compare_as_integer:          ; should work in general for any sign/magnitude integer
    vpcmpgtq xmm2, xmm1, xmm0     ; reversed order of comparison: x1>x0 == x0<x1
    vpand    xmm3, xmm1, xmm0     ; we only care about the MSB of the 64-bit integer
    vpxor    xmm2, xmm3           ; flip if x0 & x1 are negative

    vpcmpeqq xmm1, xmm0
    vpor     xmm2, xmm1
       ; top bits of XMM2 hold the boolean result for each SIMD element
       ; suitable for use with blendvpd

    vmovmskpd eax, xmm2           ; low bit of EAX = valid, high bit might be garbage
    and      eax, 1          ; optional depending on use-case
     ; EAX=1 if x0 bitwise_eq x1 or sign/magnitude x1 > x0
    ret

使用 AVX512VL,vpternlogq 可以替换所有 3 个 AND/XOR/OR 操作;它可以实现 3 个输入的任意布尔函数。 (y_gt_x) ^ (x&amp;y) | y_eq_x

没有 SSE4.2,或者只是作为一个标量无分支策略,我想出了这个。 (例如,如果值实际上在内存中,那么您可以从 XMM regs 执行 mov 加载而不是 movq)。

;; works on its own, or as the fallback after ucomisd/jz
compare_as_integer:
    movq     rcx, xmm0
    movq     rsi, xmm1

    xor      eax, eax
    cmp      rcx, rsi
   ; je  bitwise equal special case would simplify the rest
    setl     al                 ; 2's complement x < y
    sete     dl
    and      rcx, rsi           ; maybe something with TEST / CMOVS?
    shr      rcx, 63
    xor      al, cl           ; flip the SETL result if both inputs were negative
    or       al, dl           ; always true on bitwise equal
    ret

EAX should make it safe to read EAX without a partial-reg stall 的异或归零即使在 P6 系列上,在用 setl 和 8 位 xoror 编写 AL 之后。 (Why doesn't GCC use partial registers?)。在大多数其他 CPU 上,这里唯一的缺点是对 RDX 旧值的错误依赖,我在sete dl 之前没有破坏它。如果我先对 EDX 进行异或归零,我们可以将 xoror 导入 EAX。

分支策略可以这样工作:

;; probably slower unless data is predictable, e.g. mostly non-negative
compare_as_integer_branchy:
    movq     rcx, xmm0
    movq     rsi, xmm1

    xor      eax, eax       ; mov eax,1 with je to a ret wouldn't avoid partial-register stalls for setl al
    cmp      rcx, rsi
    je      .flip_result        ; return 1
    setl     al                 ; 2's complement x < y

    test     rcx, rsi
    js     .flip_result         ; if (x&y both negative)
    ret

.flip_result:         ; not bitwise EQ, and both inputs negative
    xor      al, 1
    ret

如果你愿意,可以混合和匹配其中的部分; AND/SHR/XOR 可以沿非相等路径使用,而不是 test+js


如果在对结果进行分支的情况下将其内联,则可以将 common(?)-case(有限且不等于)分支放在特殊情况处理之前。但是特殊情况包括有序的&lt;,因此在 ZF=1 上的希望可预测的分支(包括 PF=1 无序的情况)可能仍然是一个好主意。

    ucomisd  xmm1, xmm0
    ja       x1_gt_x0                ; CF==0 && ZF==0
    ; maybe unordered, maybe -0 vs +0, maybe just x1 < x0

脚注 1:NaN 编码作为总顺序的一部分

FP 值(及其符号/幅度编码)围绕零对称。符号位始终是符号位,即使对于 NaN 也是如此,因此可以以相同的方式处理。

  • 当然,最小幅度为 +-0.0:所有指数和尾数位为零。
  • 次正规有一个零指数字段(最小值),这意味着尾数的前导零。显式部分非零。幅度与尾数成线性关系。 (零实际上只是次正规的特例。)
  • 标准化的数字从指数 = 1 到指数
  • +- 无穷大有指数 = 全一,尾数 = 0
  • +- NaN 的指数 = 全一,尾数 = 非零
    • 在 x86 上,sNaN 已清除尾数的最高位。 Rest 是在任何地方至少有 1 个设置位的有效载荷(否则它是一个 Inf)。
    • 在 x86 上,qNaN 具有尾数集的最高位。休息是有效载荷

https://cwiki.apache.org/confluence/display/stdcxx/FloatingPoint(链接自Are the bit patterns of NaNs really hardware-dependent?)在其他几个 ISA 上显示了一些 sNaN 和 qNaN 编码。有些与 x86 不同,但 POWER 和 Alpha 具有为 qNaN 设置的尾数的 MSB,因此它们的整数幅度比任何 sNaN 都大。

PA-RISC 选择了相反的方式,因此在那个(过时的)ISA 上实现全序谓词需要为 FP-compare 无序情况做额外的工作;如果它们中的任何一个是任何类型的 NaN,则在进行整数比较之前,可能在两个值中翻转该位可能会起作用。

(我提到这一点是因为相同的算法可以用于可能不专门用于 x86 的高级语言中。但您可能希望保留它并始终以相同的方式处理相同的二进制位模式,即使这意味着在某些平台上 qNaN


PS:我知道“significand”在技术上更正确,但“mantissa”的音节更少,我更喜欢它,并且在这种情况下很好理解。

【讨论】:

  • 您知道在浮点寄存器上使用 pcmp... 操作是否“更好”/“更快”还是先将它们移动到 GP regs 然后对它们进行整数运算?
  • 好的,所以在伪代码中逻辑应该是这样的,对吧? gist.github.com/soc/455aa2be8250537da3b5b3ac1d13578e
  • @soc: 如果你正在使用结果,例如对于 SIMD 寄存器的混合,如果你能有效地做到这一点,那么很可能留在 SIMD 中是件好事,特别是如果你想真正做 SIMD 并一次产生 2 个结果。例如用于 SIMD 排序网络。如果你想对结果进行分支,你需要在某个时候在 GP regs 中使用它。在这两个操作数的movq 之前或并行地做一些工作可能是值得的。 OTOH,GP-integer 比较在很多端口上运行,但 CMOV 使用起来可能很笨重,而 SIMD 比较产生掩码结果。特别是使用 AVX1 来避免 movdqa reg。副本
  • 这是一个相当简单的用例,我只想有一个方法compareTotal(x: Double, y: Double): Int 告诉我第一个值是小于、等于还是大于第二个值。
  • @soc:你甚至不打算将它内联到调用它的循环中??你为什么要在asm中手动编写它?由于您需要将结果作为Int,因此您可以考虑将 FP 位模式作为 64 位 Long,因此调用者首先将它们传递到整数寄存器中。然后,如果调用者从内存中加载它们,它可以直接加载到 RDI 和 RSI 而不是 XMM regs。 (或适用于 Windows x64 的 RDX 和 RCX)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-04-06
  • 1970-01-01
  • 1970-01-01
  • 2014-01-26
  • 1970-01-01
  • 2014-04-01
  • 2011-03-25
相关资源
最近更新 更多