是否有文档描述Clang如何处理过多的浮点精度?

时间:2013-07-15 20:56:11

标签: c floating-point clang c99 extended-precision

当允许使用的唯一浮点指令是387个时,以合理的代价提供严格的IEEE 754语义几乎是不可能的(*)。当希望保持FPU在完整的64位有效位数上工作时,特别困难,以便long double类型可用于扩展精度。通常的“解决方案”是以唯一可用的精度进行中间计算,并在或多或少明确定义的情况下转换为较低的精度。

根据Joseph S. Myers在2008 post to the GCC mailing list中的解释,GCC的最新版本处理中间计算中的过度精确度。根据我的理解,这个描述使用gcc -std=c99 -mno-sse2 -mfpmath=387编译的程序完全可以预测到最后一点。如果它偶然没有,那就是一个错误而且它将被修复:约瑟夫S.迈尔斯在他的帖子中声明的意图是使其可预测。

是否记录了Clang如何处理超出精确度(例如何时使用选项-mno-sse2),以及在哪里?

(*)编辑:这是夸大其词。当允许一个配置x87 FPU使用53位有效数字时,模拟二进制64是slightly annoying but not that difficult


在下面的R ..评论之后,这是我与我最近版本的Clang的短暂互动的日志:

Hexa:~ $ clang -v
Apple clang version 4.1 (tags/Apple/clang-421.11.66) (based on LLVM 3.1svn)
Target: x86_64-apple-darwin12.4.0
Thread model: posix
Hexa:~ $ cat fem.c
#include <stdio.h>
#include <math.h>
#include <float.h>
#include <fenv.h>

double x;
double y = 2.0;
double z = 1.0;

int main(){
  x = y + z;
  printf("%d\n", (int) FLT_EVAL_METHOD);
}
Hexa:~ $ clang -std=c99 -mno-sse2 fem.c
Hexa:~ $ ./a.out 
0
Hexa:~ $ clang -std=c99 -mno-sse2 -S fem.c
Hexa:~ $ cat fem.s 
…
    movl    $0, %esi
    fldl    _y(%rip)
    fldl    _z(%rip)
    faddp   %st(1)
    movq    _x@GOTPCREL(%rip), %rax
    fstpl   (%rax)
…

2 个答案:

答案 0 :(得分:15)

这不能回答最初提出的问题,但如果您是处理类似问题的程序员,这个答案可能对您有所帮助。

我真的不知道感知到的困难在哪里。提供严格的IEEE-754 binary64语义,同时限制为80387浮点数学,并保留80位长双精度计算,似乎遵循GCC-4.6.3和clang-3.0(基于LLVM)的明确指定的C99转换规则3.0)。

编辑添加:然而,Pascal Cuoq是正确的:gcc-4.6.3或clang-llvm-3.0实际上都没有为<38>浮点数学正确地执行正确的规则。给定正确的编译器选项,规则正确应用于在编译时计算的表达式,但不适用于运行时表达式。有一些解决方法,在下面的休息后列出。

我做分子动力学模拟代码,并且非常熟悉可重复性/可预测性要求,并且希望尽可能保持最大精度,所以我声称我知道我在这里谈论的是什么。这个答案应该表明工具存在且易于使用;这些问题源于不了解或不使用这些工具。

(我喜欢的一个首选示例是Kahan求和算法。使用C99和适当的转换(向维基百科示例代码添加转换),根本不需要任何技巧或额外的临时变量。无论编译器优化级别如何,实现都有效,包括-O3-Ofast。)

C99明确指出(例如5.4.2.2),转换和赋值都删除了所有额外的范围和精度。这意味着您可以通过将计算期间使用的临时变量定义为long double来使用long double算术,同时将输入变量转换为该类型;每当需要IEEE-754二进制64时,只需转换为double

On&#39; 387,演员阵容在上述两个编译器上生成一个赋值和一个载荷;这确实将80位值正确地舍入到IEEE-754 binary64。在我看来,这笔费用非常合理。确切的时间取决于架构和周围的代码;通常它可以并且可以与其他代码交错以将成本降低到可忽略的水平。当MMX,SSE或AVX可用时,它们的寄存器与80位80387寄存器分开,通常通过将值移到MMX / SSE / AVX寄存器来完成转换。

(我更喜欢生产代码使用特定的浮点类型,比如tempdouble等,用于临时变量,因此可以根据doublelong double来定义它关于架构和所需的速度/精度权衡。)

简而言之:

  

不要因为所有变量和文字常量都是(expression)而假设double具有(double)(expression)精度。如果您希望结果为double精度,请将其写为expr1

这也适用于复合表达式,有时可能导致具有多级强制转换的笨拙表达式。

如果您希望以80位精度计算expr2long double expr1; long double expr2; double product = (double)(expr1) * (double)(expr2); ,但还需要首先舍入到64位的乘积,请使用

product

注意,double other = expr1 * expr2; 计算为两个64位值的乘积; 以80位精度计算,然后向下舍入。以80位精度计算产品,然后向下舍入,将是

double       other = (double)((long double)(expr1) * (long double)(expr2));

或添加描述性演员,告诉您具体发生的事情,

product

很明显otherfloat经常不同。

如果您使用混合的32位/ 64位/ 80位/ 128位浮点值,C99转换规则只是您必须学习的另一个工具。实际上,如果在大多数体系结构中混合使用binary32和binary64浮点数(double#include <stdio.h> #define TEST(eq) printf("%-56s%s\n", "" # eq ":", (eq) ? "true" : "false") int main(void) { double d = 1.0 / 10.0; long double ld = 1.0L / 10.0L; printf("sizeof (double) = %d\n", (int)sizeof (double)); printf("sizeof (long double) == %d\n", (int)sizeof (long double)); printf("\nExpect true:\n"); TEST(d == (double)(0.1)); TEST(ld == (long double)(0.1L)); TEST(d == (double)(1.0 / 10.0)); TEST(ld == (long double)(1.0L / 10.0L)); TEST(d == (double)(ld)); TEST((double)(1.0L/10.0L) == (double)(0.1)); TEST((long double)(1.0L/10.0L) == (long double)(0.1L)); printf("\nExpect false:\n"); TEST(d == ld); TEST((long double)(d) == ld); TEST(d == 0.1L); TEST(ld == 0.1); TEST(d == (long double)(1.0L / 10.0L)); TEST(ld == (double)(1.0L / 10.0)); return 0; } ),就会遇到完全相同的问题!

或许改写Pascal Cuoq的探索代码,以正确应用施法规则,使这更清晰?

sizeof (double) = 8
sizeof (long double) == 12

Expect true:
d == (double)(0.1):                                     true
ld == (long double)(0.1L):                              true
d == (double)(1.0 / 10.0):                              true
ld == (long double)(1.0L / 10.0L):                      true
d == (double)(ld):                                      true
(double)(1.0L/10.0L) == (double)(0.1):                  true
(long double)(1.0L/10.0L) == (long double)(0.1L):       true

Expect false:
d == ld:                                                false
(long double)(d) == ld:                                 false
d == 0.1L:                                              false
ld == 0.1:                                              false
d == (long double)(1.0L / 10.0L):                       false
ld == (double)(1.0L / 10.0):                            false

GCC和clang的输出是

ld == 0.1

除了最近版本的GCC促使ld == 0.1L的右侧首先加倍(即true},产生long double,而使用SSE / AVX,{{ 1}}是128位。

对于纯粹的387测试,我使用了

gcc -W -Wall -m32 -mfpmath=387 -mno-sse ... test.c -o test
clang -W -Wall -m32 -mfpmath=387 -mno-sse ... test.c -o test

将各种优化标记组合设为...,包括-fomit-frame-pointer-O0-O1-O2-O3和{{1 }}

除了-Os大小(以及当前GCC版本的long double)之外,使用任何其他标志或C99编译器应该会得到相同的结果。如果你遇到任何分歧,我会非常感激听到他们;我可能需要警告我的用户这样的编译器(编译器版本)。请注意,Microsoft不支持C99,因此它们对我来说完全没有意义。


Pascal Cuoq确实在下面的评论链中提出了一个有趣的问题,我没有立即认出来。

在评估表达式时,GCC和具有ld == 1.0的clang都指定使用80位精度计算所有表达式。这导致例如

-mfpmath=387

产生不正确的结果,因为二进制结果中间的那些字符串恰好位于53位和64位尾数(分别为64位和80位浮点数)之间。所以,虽然预期的结果是

7491907632491941888 = 0x1.9fe2693112e14p+62 = 110011111111000100110100100110001000100101110000101000000000000
5698883734965350400 = 0x1.3c5a02407b71cp+62 = 100111100010110100000001001000000011110110111000111000000000000

7491907632491941888 * 5698883734965350400 = 42695510550671093541385598890357555200 = 100000000111101101101100110001101000010100100001011110111111111111110011000111000001011101010101100011000000000000000000000000

仅使用42695510550671088819251326462451515392 = 0x1.00f6d98d0a42fp+125 = 100000000111101101101100110001101000010100100001011110000000000000000000000000000000000000000000000000000000000000000000000000 获得的结果是

-std=c99 -m32 -mno-sse -mfpmath=387

理论上,您应该能够通过使用选项告诉gcc和clang强制执行正确的C99舍入规则

42695510550671098263984292201741942784 = 0x1.00f6d98d0a43p+125 = 100000000111101101101100110001101000010100100001100000000000000000000000000000000000000000000000000000000000000000000000000000

但是,这只影响编译器优化的表达式,并且似乎根本不修复387处理。如果您使用例如-std=c99 -m32 -mno-sse -mfpmath=387 -ffloat-store -fexcess-precision=standard clang -O1 -std=c99 -m32 -mno-sse -mfpmath=387 -ffloat-store -fexcess-precision=standard test.c -o test && ./test test.c(double)d1 * (double)d2 ,您将根据IEEE-754规则获得正确的结果 - 但这只是因为编译器优化了表达式,而根本不使用387。

简单地说,而不是计算

(double)((long double)d1 * (long double)d2)

gcc和clang实际上都告诉“387”计算

FLT_EVAL_METHOD=2

这确实是我相信这是一个影响gcc-4.6.3和clang-llvm-3.0的编译器错误,也是一个容易复制的错误。 (Pascal Cuoq指出libm意味着对双精度参数的操作总是以扩展精度完成,但我看不出任何理智的理由 - 除了必须在{39}上重写部分-std=c99 -ffloat-store -fexcess-precision=standard ; 387 - 在C99中做到这一点并考虑IEEE-754规则是硬件可以实现的!毕竟,编译器可以通过修改387控件轻松实现正确的操作用于匹配表达式精度的单词。并且,鉴于编译器选项应该强制执行此行为 - FLT_EVAL_METHOD=2 - 如果实际需要fesetround(FE_TOWARDZERO)行为,则没有意义,也没有向后兼容性问题。)重要的是要注意,给定正确的编译器标志,在编译时评估的表达式会被正确评估,并且只有在运行时计算的表达式才会得到不正确的结果。

最简单的解决方法和便携式解决方法是使用fenv.h(来自x = [0,1))将所有结果舍入为零。

在某些情况下,向零舍入可能有助于预测性和病理性病例。特别是,对于像__FPU_SETCW()这样的区间,向零舍入意味着通过舍入从未达到上限;如果您评估,例如,分段样条。

对于其他舍入模式,您需要直接控制387硬件。

您可以使用#include <fpu_control.h>中的precision.c或开放代码。例如,#include <stdlib.h> #include <stdio.h> #include <limits.h> #define FP387_NEAREST 0x0000 #define FP387_ZERO 0x0C00 #define FP387_UP 0x0800 #define FP387_DOWN 0x0400 #define FP387_SINGLE 0x0000 #define FP387_DOUBLE 0x0200 #define FP387_EXTENDED 0x0300 static inline void fp387(const unsigned short control) { unsigned short cw = (control & 0x0F00) | 0x007f; __asm__ volatile ("fldcw %0" : : "m" (*&cw)); } const char *bits(const double value) { const unsigned char *const data = (const unsigned char *)&value; static char buffer[CHAR_BIT * sizeof value + 1]; char *p = buffer; size_t i = CHAR_BIT * sizeof value; while (i-->0) *(p++) = '0' + !!(data[i / CHAR_BIT] & (1U << (i % CHAR_BIT))); *p = '\0'; return (const char *)buffer; } int main(int argc, char *argv[]) { double d1, d2; char dummy; if (argc != 3) { fprintf(stderr, "\nUsage: %s 7491907632491941888 5698883734965350400\n\n", argv[0]); return EXIT_FAILURE; } if (sscanf(argv[1], " %lf %c", &d1, &dummy) != 1) { fprintf(stderr, "%s: Not a number.\n", argv[1]); return EXIT_FAILURE; } if (sscanf(argv[2], " %lf %c", &d2, &dummy) != 1) { fprintf(stderr, "%s: Not a number.\n", argv[2]); return EXIT_FAILURE; } printf("%s:\td1 = %.0f\n\t %s in binary\n", argv[1], d1, bits(d1)); printf("%s:\td2 = %.0f\n\t %s in binary\n", argv[2], d2, bits(d2)); printf("\nDefaults:\n"); printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2)); printf("\nExtended precision, rounding to nearest integer:\n"); fp387(FP387_EXTENDED | FP387_NEAREST); printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2)); printf("\nDouble precision, rounding to nearest integer:\n"); fp387(FP387_DOUBLE | FP387_NEAREST); printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2)); printf("\nExtended precision, rounding to zero:\n"); fp387(FP387_EXTENDED | FP387_ZERO); printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2)); printf("\nDouble precision, rounding to zero:\n"); fp387(FP387_DOUBLE | FP387_ZERO); printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2)); return 0; }

clang -std=c99 -m32 -mno-sse -mfpmath=387 -O3 -W -Wall precision.c -o precision
./precision 7491907632491941888 5698883734965350400

7491907632491941888:    d1 = 7491907632491941888
        0100001111011001111111100010011010010011000100010010111000010100 in binary
5698883734965350400:    d2 = 5698883734965350400
        0100001111010011110001011010000000100100000001111011011100011100 in binary

Defaults:
Product = 42695510550671098263984292201741942784
          0100011111000000000011110110110110011000110100001010010000110000 in binary

Extended precision, rounding to nearest integer:
Product = 42695510550671098263984292201741942784
          0100011111000000000011110110110110011000110100001010010000110000 in binary

Double precision, rounding to nearest integer:
Product = 42695510550671088819251326462451515392
          0100011111000000000011110110110110011000110100001010010000101111 in binary

Extended precision, rounding to zero:
Product = 42695510550671088819251326462451515392
          0100011111000000000011110110110110011000110100001010010000101111 in binary

Double precision, rounding to zero:
Product = 42695510550671088819251326462451515392
          0100011111000000000011110110110110011000110100001010010000101111 in binary

使用clang-llvm-3.0进行编译和运行,我得到了正确的结果,

fp387()

换句话说,您可以使用libm.a设置精度和舍入模式来解决编译器问题。

缺点是可以编写一些数学库(libm.sofpu_control.h),假设中间结果总是以80位精度计算。至少x86_64上的GNU C库libm具有注释&#34; libm需要扩展精度&#34; 。幸运的是,您可以采用例如'387'的实施方案。 GNU C库,如果需要math.h功能,可以在头文件中实现它们或编写已知工作{{1}};事实上,我想我可以帮助那里。

答案 1 :(得分:5)

为了记录,下面是我通过实验发现的。以下程序显示了使用Clang编译时的各种行为:

#include <stdio.h>

int r1, r2, r3, r4, r5, r6, r7;

double ten = 10.0;

int main(int c, char **v)
{
  r1 = 0.1 == (1.0 / ten);
  r2 = 0.1 == (1.0 / 10.0);
  r3 = 0.1 == (double) (1.0 / ten);
  r4 = 0.1 == (double) (1.0 / 10.0);
  ten = 10.0;
  r5 = 0.1 == (1.0 / ten);
  r6 = 0.1 == (double) (1.0 / ten);
  r7 = ((double) 0.1) == (1.0 / 10.0);
  printf("r1=%d r2=%d r3=%d r4=%d r5=%d r6=%d r7=%d\n", r1, r2, r3, r4, r5, r6, r7);
}

结果因优化级别而异:

$ clang -v
Apple LLVM version 4.2 (clang-425.0.24) (based on LLVM 3.2svn)
$ clang -mno-sse2 -std=c99  t.c && ./a.out
r1=0 r2=1 r3=0 r4=1 r5=1 r6=0 r7=1
$ clang -mno-sse2 -std=c99 -O2  t.c && ./a.out
r1=0 r2=1 r3=0 r4=1 r5=1 r6=1 r7=1

(double)区分r5r6的广告-O2-O0以及变量r3和{{1}无效}}。结果r4与所有优化级别的r1不同,而r5仅与r6的{​​{1}}不同。