添加两个浮点数

时间:2014-07-02 21:48:20

标签: gcc floating-point clang c99 fenv

我想计算两个IEEE 754二进制64号码的总和。为此我在下面写了C99程序:

#include <stdio.h>
#include <fenv.h>
#pragma STDC FENV_ACCESS ON

int main(int c, char *v[]){
  fesetround(FE_UPWARD);
  printf("%a\n", 0x1.0p0 + 0x1.0p-80);
}

但是,如果我使用各种编译器编译并运行我的程序:

$ gcc -v
…
gcc version 4.2.1 (Apple Inc. build 5664)
$ gcc -Wall -std=c99 add.c && ./a.out 
add.c:3: warning: ignoring #pragma STDC FENV_ACCESS
0x1p+0
$ clang -v
Apple clang version 1.5 (tags/Apple/clang-60)
Target: x86_64-apple-darwin10
Thread model: posix
$ clang -Wall -std=c99 add.c && ./a.out 
add.c:3:14: warning: pragma STDC FENV_ACCESS ON is not supported, ignoring
      pragma [-Wunknown-pragmas]
#pragma STDC FENV_ACCESS ON
             ^
1 warning generated.
0x1p+0

它不起作用! (我期待结果0x1.0000000000001p0)。

实际上,计算是在编译时以默认的舍入到最近模式完成的:

$ clang -Wall -std=c99 -S add.c && cat add.s
add.c:3:14: warning: pragma STDC FENV_ACCESS ON is not supported, ignoring
      pragma [-Wunknown-pragmas]
#pragma STDC FENV_ACCESS ON
             ^
1 warning generated.
…
LCPI1_0:
    .quad   4607182418800017408
…
    callq   _fesetround
    movb    $1, %cl
    movsd   LCPI1_0(%rip), %xmm0
    leaq    L_.str(%rip), %rdx
    movq    %rdx, %rdi
    movb    %cl, %al
    callq   _printf
…
L_.str:
    .asciz   "%a\n"

是的,我确实看到了每个编译器发出的警告。我知道在线的比例上打开或关闭适用的优化可能是棘手的。如果可能的话,我仍然希望在文件的范围内关闭它们,这足以解决我的问题。

我的问题是:我应该使用哪些命令行选项与GCC或Clang一起编译一个C99编译单元,其中包含用于以默认的FPU舍入模式执行的代码?

题外话

在研究这个问题时,我发现这个GCC C99 compliance page,包含下面的条目,我将在这里留下以防其他人觉得它很有趣。 GRRRR。

floating-point      |     |
environment access  | N/A | Library feature, no compiler support required.
in <fenv.h>         |     |

2 个答案:

答案 0 :(得分:4)

我无法找到任何能够满足您需求的命令行选项。但是,我确实找到了一种重写代码的方法,这样即使进行了最大程度的优化(甚至架构优化),GCC和Clang都不会在编译时计算该值。相反,这会强制它们输出将在运行时计算值的代码。

C:

#include <fenv.h>
#include <stdio.h>

#pragma STDC FENV_ACCESS ON

// add with rounding up
double __attribute__ ((noinline)) addrup (double x, double y) {
  int round = fegetround ();
  fesetround (FE_UPWARD);
  double r = x + y;
  fesetround (round);   // restore old rounding mode
  return r;
}

int main(int c, char *v[]){
  printf("%a\n", addrup (0x1.0p0, 0x1.0p-80));
}

这导致GCC和Clang的这些输出,即使使用最大和架构优化:

gcc -S -x c -march=corei7 -O3Godbolt GCC):

addrup:
        push    rbx
        sub     rsp, 16
        movsd   QWORD PTR [rsp+8], xmm0
        movsd   QWORD PTR [rsp], xmm1
        call    fegetround
        mov     edi, 2048
        mov     ebx, eax
        call    fesetround
        movsd   xmm1, QWORD PTR [rsp]
        mov     edi, ebx
        movsd   xmm0, QWORD PTR [rsp+8]
        addsd   xmm0, xmm1
        movsd   QWORD PTR [rsp], xmm0
        call    fesetround
        movsd   xmm0, QWORD PTR [rsp]
        add     rsp, 16
        pop     rbx
        ret
.LC2:
        .string "%a\n"
main:
        sub     rsp, 8
        movsd   xmm1, QWORD PTR .LC0[rip]
        movsd   xmm0, QWORD PTR .LC1[rip]
        call    addrup
        mov     edi, OFFSET FLAT:.LC2
        mov     eax, 1
        call    printf
        xor     eax, eax
        add     rsp, 8
        ret
.LC0:
        .long   0
        .long   988807168
.LC1:
        .long   0
        .long   1072693248

clang -S -x c -march=corei7 -O3Godbolt GCC):

addrup:                                 # @addrup
        push    rbx
        sub     rsp, 16
        movsd   qword ptr [rsp], xmm1   # 8-byte Spill
        movsd   qword ptr [rsp + 8], xmm0 # 8-byte Spill
        call    fegetround
        mov     ebx, eax
        mov     edi, 2048
        call    fesetround
        movsd   xmm0, qword ptr [rsp + 8] # 8-byte Reload
        addsd   xmm0, qword ptr [rsp]   # 8-byte Folded Reload
        movsd   qword ptr [rsp + 8], xmm0 # 8-byte Spill
        mov     edi, ebx
        call    fesetround
        movsd   xmm0, qword ptr [rsp + 8] # 8-byte Reload
        add     rsp, 16
        pop     rbx
        ret

.LCPI1_0:
        .quad   4607182418800017408     # double 1
.LCPI1_1:
        .quad   4246894448610377728     # double 8.2718061255302767E-25
main:                                   # @main
        push    rax
        movsd   xmm0, qword ptr [rip + .LCPI1_0] # xmm0 = mem[0],zero
        movsd   xmm1, qword ptr [rip + .LCPI1_1] # xmm1 = mem[0],zero
        call    addrup
        mov     edi, .L.str
        mov     al, 1
        call    printf
        xor     eax, eax
        pop     rcx
        ret

.L.str:
        .asciz  "%a\n"

现在有一个更有趣的部分:为什么这样做?

好吧,当他们(GCC和/或Clang)编译代码时,他们会尝试查找和替换可在运行时计算的值。这称为常量传播。如果您只是编写了另一个函数,则不再发生传播,因为它不应该跨越函数。

但是,如果他们看到一个功能,他们理论上可以用代替函数调用代替代码,他们可以这样做。这称为函数内联。如果函数内联将对函数起作用,我们说该函数是(惊讶) inlinable

如果函数总是为给定的一组输入返回相同的结果,那么它被认为是。我们还说它没有副作用(意味着它不会改变环境)。

现在,如果一个函数完全无法使用(意味着它不会对外部库进行任何调用,除了GCC和Clang中包含的一些默认值 - libclibm等。并且是纯粹的,然后他们将恒定传播应用于函数。

换句话说,如果我们不希望它们通过函数调用传播常量,我们可以做以下两件事之一:

  • 使功能显得不纯:
    • 使用文件系统
    • 从某处随机输入做一些废话魔法
    • 使用网络
    • 使用某种类型的系统调用
    • 从GCC和/或Clang未知的外部库中调用某些内容
  • 使功能不完全无法使用
    • 从GCC和/或Clang未知的外部库中调用某些内容
    • 使用__attribute__ ((noinline))

现在,最后一个是最简单的。正如您可能猜测的那样,__attribute__ ((noinline))将该功能标记为不可嵌入。由于我们可以利用这一点,我们所要做的就是创建另一个函数来执行我们想要的任何计算,用__attribute__ ((noinline))标记它,然后调用它。

编译时,它们不会违反内联和扩展的常量传播规则,因此,该值将在运行时使用适当的舍入模式设置进行计算。

答案 1 :(得分:1)

clang或gcc -frounding-math告诉他们代码可能以非默认的舍入模式运行。 这不是完全安全的(假设 same 舍入模式始终处于活动状态),但总比没有好。在某些情况下,您可能仍需要使用volatile来避免CSE,或者可能是其他答案中的noinline包装技巧,实际上,如果将其限制为单个操作,效果会更好。


您注意到,GCC不支持#pragma STDC FENV_ACCESS ON。默认行为类似于FENV_ACCESS OFF相反,您必须使用命令行选项(或按功能属性)来控制FP优化。

https://gcc.gnu.org/wiki/FloatingPointMath 中所述,默认情况下-frounding-math未启用 ,因此GCC在进行恒定传播和其他操作时会采用默认的舍入模式编译时进行优化。

但是使用gcc -O3 -frounding-math,会阻止持续传播。即使您fesetround;实际发生的情况是,如果甚至在调用main之前已经将舍入模式设置为其他值,则GCC会使asm安全。

但不幸的是,正如Wiki所指出的那样,GCC仍然假定在所有地方都有效相同的舍入模式(GCC bug #34678)。这意味着 会在调用fesetround之前/之后CSE对相同输入进行两次计算,因为它不会将fesetround视为特殊。

#include <fenv.h>
#pragma STDC FENV_ACCESS ON

void foo(double *restrict out){
    out[0] = 0x1.0p0 + 0x1.0p-80;
    fesetround(FE_UPWARD);
    out[1] = 0x1.0p0 + 0x1.0p-80;
}

compiles as follows (Godbolt)和gcc10.2(与clang10.1基本相同)。还包括您的main,它确实可以实现所需的组合。

foo:
        push    rbx
        mov     rbx, rdi
        sub     rsp, 16
        movsd   xmm0, QWORD PTR .LC1[rip]
        addsd   xmm0, QWORD PTR .LC0[rip]     # runtime add
        movsd   QWORD PTR [rdi], xmm0         # store out[0]
        mov     edi, 2048
        movsd   QWORD PTR [rsp+8], xmm0       # save a local temporary for later
        call    fesetround
        movsd   xmm0, QWORD PTR [rsp+8]
        movsd   QWORD PTR [rbx+8], xmm0       # store the same value, not recalc
        add     rsp, 16
        pop     rbx
        ret

在其他答案下,如果您的noinline函数在更改取整模式前后进行相同的数学运算,则这是相同的问题@Marc Glisse warned about in comments

(而且GCC第一次选择不{em> 调用fesetround之前做数学运算也是部分幸运的事,因此只需要溢出结果即可,而不必浪费两个输入。 x86-64 System V没有任何保留呼叫的XMM规则。