36

我正在寻找有关GCC/x86 的详细信息(更多的是出于好奇而不是因为实际问题)long double__float128

可能很少有人会需要这些(我只是,有史以来第一次,真正需要 a double),但我想知道你的工具箱里有什么以及它是关于什么的仍然是值得的(而且很有趣)。

鉴于此,请原谅我有些开放的问题:

  1. 有人可以解释这些类型的实现原理和预期用途,也可以相互比较吗?例如,它们是否因为标准允许类型而“令人尴尬的实现”,如果它们只是与 相同的精度double,或者它们打算作为一流类型,有人可能会抱怨?
  2. 或者,有人可以分享一个好的、可用的网络参考吗?谷歌搜索"long double" site:gcc.gnu.org/onlinedocs并没有给我太多真正有用的东西。
  3. 假设常见的口头禅“如果你认为你需要双精度,你可能不了解浮点数”不适用,即你真的需要比精度更高的精度float,而且不关心是 8 字节还是 16 字节的内存烧毁...是否可以合理地期望一个人也可以直接跳到long double或者__float128没有double显着的性能影响?
  4. 历史上,当值在内存和寄存器之间移动时,英特尔 CPU 的“扩展精度”特性一直是令人讨厌的意外之源。如果实际存储了 96 位,则该long double类型应消除此问题。另一方面,我知道该long double类型与 是互斥的-mfpmath=sse,因为 SSE 中没有“扩展精度”之类的东西。__float128,另一方面,在 SSE 数学上应该可以很好地工作(尽管在没有四精度指令的情况下肯定不是在 1:1 指令库上)。我的这些假设是对的吗?

(3. 和 4. 可能可以通过在分析和反汇编上花费一些工作来弄清楚,但也许其他人之前也有同样的想法并且已经完成了这项工作。)

背景(这是 TL;DR 部分):
我最初偶然发现long double是因为我正在查找DBL_MAX<float.h>顺便说LDBL_MAX一下在下一行。“哦,看,GCC 实际上有 128 位双打,不是我需要它们,但是……很酷”是我的第一个想法。惊喜,惊喜:sizeof(long double)返回 12……等等,你是说 16?

毫不奇怪,C 和 C++ 标准没有给出非常具体的类型定义。C99 (6.2.5 10) 表示的数字是C++03 状态 (3.9.1 8)double的子集,其精度至少与(这是同一件事,只是措辞不同)。基本上,标准将所有内容留给实现,与、和.long doublelong doubledoublelongintshort

维基百科说 GCC 使用“x86 处理器上的 80 位扩展精度,而不管使用的物理存储如何”

GCC 文档在同一页面上声明,由于 i386 ABI,类型的大小为 96 位,但任何选项(嗯?什么?)启用的精度不超过 80 位,还有 Pentium 和更新版本处理器希望它们对齐为 128 位数字。这是 64 位下的默认设置,可以在 32 位下手动启用,从而产生 32 位的零填充。

运行测试的时间:

#include <stdio.h>
#include <cfloat>

int main()
{
#ifdef  USE_FLOAT128
    typedef __float128  long_double_t;
#else
    typedef long double long_double_t;
#endif

long_double_t ld;

int* i = (int*) &ld;
i[0] = i[1] = i[2] = i[3] = 0xdeadbeef;

for(ld = 0.0000000000000001; ld < LDBL_MAX; ld *= 1.0000001)
    printf("%08x-%08x-%08x-%08x\r", i[0], i[1], i[2], i[3]);

return 0;
}

使用 时long double,输出看起来有点像这样,标记的数字是恒定的,而所有其他数字最终都会随着数字越来越大而变化:

5636666b-c03ef3e0-00223fd8-deadbeef
                  ^^       ^^^^^^^^

这表明它不是80 位数字。一个 80 位数字有 18 个十六进制数字。我看到 22 个十六进制数字发生了变化,这看起来更像是一个 96 位数字(24 个十六进制数字)。它也不是一个 128 位的数字,因为没有被触及,这与返回 120xdeadbeef是一致的。sizeof

的输出__int128看起来真的只是一个 128 位的数字。所有位最终都会翻转。

如文档所示,使用 32 位零填充与 128 位不-m128bit-long-double对齐。它也不使用,但确实似乎与 128 位对齐,并用值(?!) 填充。long double__int1280x7ffdd000

此外,LDBL_MAX, 似乎对+inf和 都long double有效__float128。在相同的位模式中添加或减去类似1.0E1001.0E2000到/从的数字。 到现在为止,我相信常量将包含最大的可表示数字,但事实并非如此(显然情况并非如此?)。我也不太确定一个 80 位数字如何可以想象128 位值......也许我在一天结束时太累了并且做错了什么。LDBL_MAX
foo_MAX +inf+inf

4

4 回答 4

23

广告 1。

这些类型旨在处理具有巨大动态范围的数字。long double 在 x87 FPU 中以本机方式实现。我怀疑 128b double 将在现代 x86s 上以软件模式实现,因为没有硬件可以在硬件中进行计算。

有趣的是,连续执行许多浮点运算是很常见的,并且中间结果实际上并不存储在声明的变量中,而是存储在 FPU 寄存器中,以利用全精度。这就是为什么比较:

double x = sin(0); if (x == sin(0)) printf("Equal!");

不安全且不能保证工作(没有额外的开关)。

广告。3.

速度会受到影响,具体取决于您使用的精度。您可以使用以下方法更改使用的 FPU 精度:

void 
set_fpu (unsigned int mode)
{
  asm ("fldcw %0" : : "m" (*&mode));
}

较短的变量会更快,更长的变量会更慢。128 位双打可能会在软件中完成,所以会慢得多。

这不仅与浪费 RAM 内存有关,还与浪费缓存有关。从 64b double 转到 80 bit double 将浪费 33% (32b) 到几乎 50% (64b) 的内存(包括缓存)。

广告 4。

另一方面,我知道 long double 类型与 -mfpmath=sse 是互斥的,因为 SSE 中没有“扩展精度”之类的东西。另一方面,__float128 应该与 SSE 数学完美配合(尽管在没有四精度指令的情况下肯定不是在 1:1 指令库上)。我在这些假设下是对的吗?

FPU 和 SSE 单元是完全独立的。您可以在使用 SSE 的同时使用 FPU 编写代码。问题是如果您将其限制为仅使用 SSE,编译器会生成什么?它会尝试使用 FPU 吗?我一直在用 SSE 进行一些编程,而 GCC 只会自己生成一个 SISD。您必须帮助它使用 SIMD 版本。__float128 可能适用于每台机器,甚至是 8 位 AVR uC。毕竟这只是摆弄比特。

十六进制表示的 80 位实际上是 20 个十六进制数字。也许未使用的位来自某些旧操作?在我的机器上,我编译了你的代码,在长模式下只有 20 位变化:66b4e0d2-ec09c1d5-00007ffe-deadbeef

128 位版本的所有位都发生了变化。看objdump它看起来好像在使用软件仿真,几乎没有 FPU 指令。

此外,LDBL_MAX 似乎对于 long double 和 __float128 都可以用作 +inf。在 LDBL_MAX 中添加或减去 1.0E100 或 1.0E2000 等数字会产生相同的位模式。到现在为止,我相信 foo_MAX 常量将保存最大的可表示数字,而不是 +inf (显然情况并非如此?)。

这似乎很奇怪……

我也不太确定一个 80 位的数字如何可以作为 128 位的值的 +inf ......也许我在一天结束时太累了并且做错了什么。

应该是延长了 在 80 位中被识别为 +inf 的模式也被转换为 128 位浮点中的 +inf。

于 2013-05-03T14:13:34.397 回答
6

IEEE-754 定义了 32 位和 64 位浮点表示以实现高效的数据存储,并定义了 80 位表示以实现高效计算。其目的是通过将参数转换为 80 位浮点值、添加它们并将结果转换回 64 位浮点类型来执行给定float f1,f2; double d1,d2;的语句。d1=f1+f2+d2;与直接对其他浮点类型执行操作相比,这将提供三个优势:

  1. 虽然在 32 位类型和 64 位类型之间的转换需要单独的代码或电路,但只需要一个“加法”实现、一个“乘法”实现、一个“平方根”实现,等等

  2. 尽管在极少数情况下,使用 80 位计算类型产生的结果可能比直接使用其他类型的结果准确度略低(在其他类型的计算会产生 511/1024ulp 错误的情况下,最坏情况舍入误差为 513/1024ulp ),使用 80 位类型的链式计算通常比使用其他类型的计算更准确——有时更准确。

  3. 在没有 FPU 的系统上,double在执行计算之前将 a 分离为单独的指数和尾数、标准化尾数以及将单独的尾数和指数转换为 adouble有点耗时。如果一个计算的结果将用作另一个计算的输入并被丢弃,则使用未打包的 80 位类型将允许省略这些步骤。

然而,为了使这种浮点数学方法有用,代码必须能够以与计算中使用的精度相同的精度存储中间结果,从而temp = d1+d2; d4=temp+d3;产生与d4=d1+d2+d3;. 据我所知,目的long double成为那种类型。不幸的是,尽管 K&R 设计了 ​​C 以便所有浮点值都以相同的方式传递给可变参数方法,但 ANSI C 打破了这一点。在最初设计的 C 中,给定 code float v1,v2; ... printf("%12.6f", v1+v2);,该printf方法不必担心是否v1+v2会产生 afloat或 a double,因为无论如何结果都会被强制转换为已知类型。此外,即使类型v1v2更改为double,该printf语句不必更改。

然而,ANSI C 要求调用的代码printf必须知道哪些参数是double,哪些是long double;许多代码(如果不是大多数代码)使用long double但编写在与它同义的平台上,但double未能使用正确的long double值格式说明符。许多编译long double器决定将long double其与double并且不提供任何存储中间计算结果的方法。由于使用扩展精度类型进行计算只有在程序员可以使用该类型时才是好的,所以许多人得出结论认为扩展精度是邪恶的,尽管只是 ANSI C 未能明智地处理可变参数参数才造成问题。

long doublePS-如果还有一个long float被定义为float可以最有效地提升参数的类型,那么的预期目的将会受益;在许多没有浮点单元的机器上可能是 48 位类型,但最佳大小的范围可以从 32 位(在具有直接执行 32 位数学的 FPU 的机器上)到 80 位(在使用IEEE-754 所设想的设计)。不过现在为时已晚。

于 2015-06-05T19:23:30.393 回答
1

它归结为 4.9999999999999999999 和 5.0 之间的差异。

  1. 尽管范围是主要区别,但重要的是精度。
  2. 在可能与 GPS 系统一起使用的大圆计算或坐标数学中将需要这些类型的数据。
  3. 由于精度比普通双精度高得多,这意味着您通常可以保留 18 位有效数字,而不会失去计算的准确性。
  4. 我相信扩展精度使用 80 位(主要用于数学处理器),因此 128 位会更准确。
于 2017-11-15T18:24:27.353 回答
0

C99 和 C++11 添加了类型float_tdouble_t它们是内置浮点类型的别名。粗略地说,float_t是在 type 的值之间进行算术运算的结果的类型float,并且double_t是在 type 的值之间进行算术运算的结果的类型double

于 2018-05-31T22:22:14.530 回答