首页 > 解决方案 > Round floating-point numbers in NASM

问题描述

I am trying to build a NASM library for a C program. I would like to round a floating-point number given as parameter.

The C function prototype would look like this:

double nearbyint(double x);

I tried using the frndint instruction but I need to push my parameter to the stack first.

Here is what I came up with (does not compile):

bits 64

section .text

global nearbyint

nearbyint:
    push    rbp
    mov     rbp, rsp

    fld     xmm0
    frndint
    fstp    xmm0

    leave
    ret

标签: assemblyfloating-pointx86-64nasm

解决方案


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

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

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

  • rint: roundsd 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文档以获取表格。抑制了不精确的异常(位 3 = 1)。

如果没有 SSE4.1 的 roundsd,看看glibc 的rint作用:它添加2^52(bit pattern 0x43300000, 0x00000000),使数字如此之大,以至于最接近的可表示doubles 是整数。因此,正常的 FP 舍入到最接近的可表示值会舍入为整数。 IEEE binary64double有 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 ....

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


推荐阅读