如何在 x86-64 上将 80 位 long double 的尾数作为 int 获取

How to get the mantissa of an 80-bit long double as an int on x86-64

frexpl 将不起作用,因为它将尾数保留为长双精度的一部分。我可以使用类型双关吗,否则会有危险吗?还有别的办法吗?

x86 的浮点数和整数字节序是小端字节序,因此有效数(又名尾数)是 80 位 x87 的低 64 位long double

在汇编中,您只需按正常方式加载,例如mov rax, [rdi]

与 IEEE binary32 (float) 或 binary64 (double) 不同,80 位长双精度数 显式 将前导 1 存储在有效数字中。 (或 0 表示次正规)。 https://en.wikipedia.org/wiki/Extended_precision#x86_extended_precision_format

所以真正的有效数字的无符号整数值(大小)与实际存储在对象表示中的值相同。

如果要签名int,太可惜了;包括符号位,它将是 65 位,但 int 在任何 x86 C 实现上都只是 32 位。

如果你想要 int64_t,你可以右移 1 以丢弃低位,为符号位腾出空间。如果设置了符号位,则执行 2 的补码求反,留下有效数值除以 2 的带符号 2 的补码表示。(IEEE FP 使用 sign/magnitude,位模式顶部有一个符号位)


在 C/C++ 中,是的,您需要输入双关语,例如与工会或 memcpy。 x86 / x86-64 上所有公开 80 位浮点的 C 实现都使用 12 或 16 字节类型,底部有 10 字节值。

请注意 MSVC 使用 long double = double,一个 64 位浮点数,因此请检查 float.hsizeof(long double) 中的 LDBL_MANT_DIG。所有 3 个 static_assert() 语句都在 MSVC 上触发,所以它们都完成了它们的工作并使我们免于将整个 binary64 double (sign/exp/mantissa) 复制到我们的 uint64_t.

// valid C11 and C++11
#include <float.h>  // float numeric-limit macros
#include <stdint.h>
#include <assert.h>  // C11 static assert
#include <string.h>  // memcpy

// inline
uint64_t ldbl_mant(long double x)
{
    // we can assume CHAR_BIT = 8 when targeting x86, unless you care about DeathStation 9000 implementations.
    static_assert( sizeof(long double) >= 10, "x87 long double must be >= 10 bytes" );
    static_assert( LDBL_MANT_DIG == 64, "x87 long double significand must be 64 bits" );

    uint64_t retval;
    memcpy(&retval, &x, sizeof(retval));
    static_assert( sizeof(retval) < sizeof(x), "uint64_t should be strictly smaller than long double" ); // sanity check for wrong types
    return retval;
}

这个compiles efficiently on gcc/clang/ICC (on Godbolt)只是一个指令作为一个独立的函数(因为调用约定在内存中传递long double)。在 x87 寄存器中使用 long double 内联代码后,它可能会编译为 TBYTE x87 存储和整数重新加载。

## gcc/clang/ICC -O3 for x86-64
ldbl_mant:
  mov rax, QWORD PTR [rsp+8]
  ret

对于 32 位,gcc 有一个奇怪的冗余复制错过优化错误,而 ICC 和 clang 没有;他们只是从函数 arg 执行 2 次加载,而不先复制。

# GCC -m32 -O3  copies for no reason
ldbl_mant:
  sub esp, 28
  fld TBYTE PTR [esp+32]            # load the stack arg
  fstp TBYTE PTR [esp]              # store a local
  mov eax, DWORD PTR [esp]
  mov edx, DWORD PTR [esp+4]        # return uint64_t in edx:eax
  add esp, 28
  ret

C99 使联合类型双关成为明确定义的行为,GNU C++ 也是如此。我认为 MSVC 也定义了它。

但是 memcpy 始终是可移植的,因此这可能是一个更好的选择,并且在我们只需要一个元素的情况下更容易阅读。

如果您还想要指数和符号位,结构和 long double 之间的联合可能会很好,除了在结构末尾对齐的填充会使它更大。不过,在 uint16_t 成员之前的 uint64_t 成员之后不太可能有填充。但是我担心 :1:15 位域,因为 IIRC 是实现定义的,位域成员的存储顺序。