【问题标题】:long double (GCC specific) and __float128long double(GCC 特定)和 __float128
【发布时间】:2018-06-01 06:22:14
【问题描述】:

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

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

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

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

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

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

不出所料,C 和 C++ 标准没有给出非常具体的类型定义。 C99 (6.2.5 10) 表示 double 的数字是 long double 的子集,而 C++03 声明 (3.9.1 8) long double 的精度至少与 double (是同一个东西,只是措辞不同)。基本上,标准将所有内容留给实现,与longintshort 的方式相同。

维基百科说 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 位数字,因为 0xdeadbeef 没有被触及,这与 sizeof 返回 12 一致。

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

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

此外,LDBL_MAX 似乎对于long double__float128 都可以作为+inf 工作。将1.0E1001.0E2000 之类的数字与LDBL_MAX 相加或相减会产生相同的位模式。
到目前为止,我认为 foo_MAX 常量将保存最大的可表示数字,not +inf(显然不是这样?)。我也不太确定一个 80 位数字如何可以想象为 128 位值的+inf...也许我在一天结束时太累了,做错了什么。

【问题讨论】:

  • 80位双精度可以存储uint64_t。它有 64 位尾数(无可选/隐式前导位)、15 位指数和一个符号位。 en.wikipedia.org/wiki/…
  • 不是在LDBL_MAX 中进行加减运算,而是尝试除以二?
  • 我没有观察到你所看到的。只有 20 个 hex 数字变化,对应扩展精度类型的 10 个字节。一个 80 位数字有 20 个十六进制数字,而不是 18

标签: gcc long-double


【解决方案1】:

广告 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 似乎可以作为 +inf 用于 long double 和 __float128。在 LDBL_MAX 中添加或减去 1.0E100 或 1.0E2000 等数字会产生相同的位模式。到目前为止,这是我的 相信 foo_MAX 常量将保持最大 不是 +inf 的可表示数字(显然不是 案子?)。

这似乎很奇怪……

我也不太确定 80 位数字是如何实现的 充当 128 位值的 +inf ......也许我最后太累了 当天做错了什么。

它可能正在扩展。在 80 位中被识别为 +inf 的模式也被转换为 128 位浮点中的 +inf。

【讨论】:

  • 1E2000L 添加到LDBL_MAX 并返回LDBL_MAX 并没有什么奇怪的。由于LDBL_MAX 超过1E4932L1E2000L 比 1ulp小很多
  • @supercat:使用比要求更高的精度是不符合要求的,并通过诸如-ffast-math 之类的开关进行控制。
  • @BenVoigt:如果 FLT_EVAL_METHOD 为 1,编译器可以合法地将 double 操作数提升为算术方法,如果为 2,则编译器可以合法提升为 long double
  • @supercat:我认为编译器甚至不符合这一点(就像你说的,= 不应该存储更高的精度值,即使 lhs 已注册),除非你禁用快速数学。
  • 为什么sin(0)的代码不安全? sin(0) 不是精确为零吗?零提升为任何其他浮点类型是否仍然精确为零?
【解决方案2】:

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

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

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

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

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

然而,ANSI C 要求调用printf 的代码必须知道哪些参数是double,哪些是long double;许多使用long double 但在与double 同义的平台上编写的代码(如果不是大多数代码)未能对long double 值使用正确的格式说明符。许多编译器决定将long doubledouble 同义,而不是让long double 成为80 位类型,除非作为可变参数方法参数传递,在这种情况下它将被强制为64 位。存储中间计算结果的方法。由于使用扩展精度类型进行计算只有在程序员可以使用该类型的情况下才是好的,所以许多人得出结论认为扩展精度是邪恶的,即使只是 ANSI C 未能明智地处理可变参数导致它出现问题。

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

【讨论】:

  • 否则它会显示一个有趣的观点。我不确定是否相信它:它谈论的是意图,几乎没有记录,因此无处可查。
  • @Ruslan:许多没有浮点单元的处理器对 IEEE-754 80 位浮点值执行操作的速度比对 IEEE-754 64 位值执行操作的速度要快。如果这不是设计的主要动力因素,这似乎是一个巨大的巧合。可以肯定的是,不能排除巧合,因为在许多情况下,任意设计决策的效果都非常好,但是由于我认为计算效率是 IEEE-754 的设计目标,因此这种效率似乎很可能是故意的。
  • @Ruslan:我不知道 IEEE 是否特别关注 C 语言,但 Kahan(IEEE-754 背后的人之一)写过关于为中间计算并在受控时间执行舍入。在某些情况下,即使在中间计算中添加一个额外的位也会对计算结果产生巨大影响(例如,使用 Heron 公式计算边为 16777215.0f、16777215.0f 和 4.0f 的三角形的面积;如果计算半周长作为“浮点数”,结果将减少约 50%)。
  • Kahan 在他的论文中反复提出但语言设计者似乎忽略了一点,那就是在精确控制的时间进行舍入很重要。为了进行准确的计算,必须有一个类型使得someType temp = a+b; result=temp+c; 将等价于result=a=b+c;。虽然 64 位和 80 位精度之间的精度差异很小,但在错误的时间执行舍入会对计算精度产生巨大影响。
  • 顺便说一句,没有 80 位 long double 的编译器(例如 MSVC)也不会以这样的精度计算:如果您查看 Windows 程序启动环境,您将看到它的 FPU 控制字设置为 53 位精度,而不是 fninit-default 64。
【解决方案3】:

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

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

【讨论】:

  • 但是...这个星球上可能的最大大圆只能有 11 个有效小数位(实际上是 10 个,考虑到 GPS 的最大分辨率),而 double 有超过 15 个有效小数位数字,并且无论如何都要以 18+ 十进制数字执行数学......?此外,如果需要精确可重现的结果,浮点也是错误的工具,定点可以。浮点数永远不会完全准确,即使是 20 倍的大小,因为有些数字是你无法表示的。
【解决方案4】:

C99 和 C++11 添加了类型 float_tdouble_t,它们是内置浮点类型的别名。大致来说,float_tfloat 类型的值之间进行算术运算的类型,double_tdouble 类型的值之间进行算术运算的结果类型。

【讨论】:

  • 这些类型旨在最大限度地提高效率而不浪费硬件。 float_t 将至少为 32 位,但如果 CPU 使用更高的精度执行操作,那么它将是那么多位。
猜你喜欢
  • 1970-01-01
  • 2011-05-04
  • 1970-01-01
  • 2018-11-30
  • 2011-09-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-30
相关资源
最近更新 更多