【问题标题】:Round floating-point numbers in NASMNASM 中的舍入浮点数
【发布时间】:2019-03-09 11:24:39
【问题描述】:

我正在尝试为 C 程序构建一个 NASM 库。我想对作为参数给出的浮点数进行四舍五入。

C 函数原型如下所示:

double nearbyint(double x);

我尝试使用frndint 指令,但我需要先将参数压入堆栈。

这是我想出的(不编译):

bits 64

section .text

global nearbyint

nearbyint:
    push    rbp
    mov     rbp, rsp

    fld     xmm0
    frndint
    fstp    xmm0

    leave
    ret

【问题讨论】:

  • 你不能直接从xmm转移到fpu。您需要先将其推送到 cpu 堆栈,然后再加载到 fpu 堆栈中。请注意,您不需要 fpu,如果您有 SSE4.1,则可以使用 ROUNDSD
  • round 是此函数的错误名称。 ISO C round() 使用一种特殊的舍入模式,该模式与 IEEE754 默认最接近(甚至作为决胜局)不同。 您尝​​试实现的函数在 ISO C/C++ 中称为 nearbyintrintround() for float in C++
  • 要解决没有 SSE4.1 的 roundsd 的不足,您可以添加然后减去一个大数字。 (glibc 的非 SSE4.1 rint 在检查 + 或 - 之后执行此操作。然后有条件地对浮点位进行修复:code.woboq.org/userspace/glibc/sysdeps/ieee754/dbl-64/…nearbyint 执行相同的操作,除了必须保存/恢复 FP 异常状态以避免引发不精确异常。
  • @PeterCordes 我需要自己实现数学函数而不使用系统函数。不过,我仍然可以按照您的建议重命名。
  • 是的,我就是这个意思。如果您正在(重新)实现标准库函数,请将它们命名为与标准定义一致。或者换一种说法,如果您的实现与标准行为匹配,则仅使用标准库函数的名称。

标签: assembly floating-point x86-64 nasm


【解决方案1】:

在 x87 和 XMM 之间获取数据的唯一方法是通过内存反弹数据。例如movsd [rsp-8] / fld qword [rsp-8] 使用红色区域。

但是您根本不需要使用 x87,如果您希望它高效的话也不应该使用。

如果您有 SSE4.1,请使用 roundsd 舍入为整数。

  • rintroundsd xmm0,xmm0, 0b0100 - 当前舍入模式(位 2 = 1),如果结果 != 输入(位 3 = 0)则设置不精确的异常标志。
  • nearbyintroundsd xmm0,xmm0, 0b1100 当前舍入模式(位 2 = 1),抑制不精确异常(位 3 = 1)。
  • roundsd xmm0,xmm0, 0b1000:舍入模式覆盖(位 2 = 0)到_MM_FROUND_TO_NEAREST_INT(位 [1:0] = 00)。请参阅roundpd docs 获取表格。抑制了不精确的异常(位 3 = 1)。

roundsd 没有 SSE4.1,看看glibc's rint 做了什么:它添加了2^52(位模式0x43300000, 0x00000000),使数字如此之大以至于最近的可表示@ 987654337@s 是整数。因此,正常的 FP 舍入到最接近的可表示值会舍入为整数。 IEEE binary64 double 有 52 个显式尾数(又名有效位)位,所以这个数字的大小并非巧合。

(对于负输入,它使用-2^52

再次减去它会得到原始数字的四舍五入版本。

glibc 实现会检查一些特殊情况(如 Inf 和 NaN),以及小于 0 的指数(即幅度小于 1.0 的输入),它会复制输入的符号位。 (我猜 -0.499 会舍入为 -0.0 而不是 0,并确保 0.499 会舍入为 +0,而不是 -0。)

使用 SSE2 实现该功能的一种简单方法是使用 pand xmm0, [signbit_mask] 隔离输入的符号位,然后在 0x43300000 的 FP 位模式中进行 OR 运算,得到 +- 2^52

default rel

;; UNTESTED.  IDK if the SIGNBIT_FIXUP does anything other than +-0.0
rint_sse2:
    ;movsd  xmm1, [signbit_mask]  ; can be cheaply constructed on the fly, unlike 2^52
    ;pand   xmm1, xmm0

    pcmpeqd  xmm1, xmm1
    psrlq    xmm1, 1             ; 0x7FFF...
%ifdef SIGNBIT_FIXUP
    movaps   xmm2, xmm1          ; copy the mask
%endif

    andnps   xmm1, xmm0          ; isolate sign bit

%ifdef SIGNBIT_FIXUP
    movaps   xmm3, xmm1          ; save another copy of the sign bit
%endif

    orps     xmm1, [big_double]  ; +-2^52
    addsd    xmm0, xmm1
    subsd    xmm0, xmm1

%ifdef SIGNBIT_FIXUP
    andps    xmm0, xmm2          ; clear the sign bit
    orps     xmm0, xmm3          ; apply the original sign
%endif
    ret

section .rodata
align 16
   big_double: dq 0x4330000000000000   ; 2^52
   ; a 16-byte load will grab whatever happens to be next
   ; if you want a packed rint(__m128d), use   times 2 dq ....

特别是如果你省略 SIGNBIT_FIXUP 的东西,这非常便宜,就 FP 延迟而言,并不比 roundsd 的 2 微秒贵多少。 (roundsd 在大多数 CPU 上与 addsd + subsd 具有相同的延迟。这几乎可以肯定不是巧合,但它确实避免了任何单独的微指令来整理符号)。

【讨论】:

  • 可能值得注意的是,幻数 (~4.5e15) 正好是 2**52,这是因为 double 实现为 IEEE -754 的binary64 格式有 52 个小数有效位。
  • @njuffa:谢谢,我认为它会是 2^52 或 53,但没有花时间找出 glibc 何时有十进制常量。
  • @PeterCordes 将此类常量表示为十六进制浮点数(例如0x1.0p+52)是一种最佳实践,但有问题的代码可能是遗留代码。此外,据我所知,ISO-C++仍然没有添加这个有用的 ISO-C99 功能,强制继续使用十进制浮点字面常量,即使十六进制更清晰。
猜你喜欢
  • 1970-01-01
  • 2020-06-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-12-07
  • 1970-01-01
相关资源
最近更新 更多