首页 文章

添加两个浮点数

提问于
浏览
18

我想计算两个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舍入模式执行的代码?

Digression

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

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

1 回答

  • 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 -O3(Godbolt 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 -O3(Godbolt 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)编译代码时,他们会尝试查找和替换可在运行时计算的值 . 这被称为 constant propagation . 如果您只是编写了另一个函数,则不再发生常量传播,因为它不应该跨越函数 .

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

    如果函数总是为给定的输入集返回相同的结果,则将其视为 pure . 我们还说它没有 side effects (意思是它不会改变环境) .

    现在,如果一个函数完全无法使用(意味着它不会对外部库进行任何调用,除了包含在GCC和Clang中的一些默认值 - libclibm 等)并且是纯粹的,那么它们将应用常量传播到功能 .

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

    • 使功能显得不纯:

    • 使用文件系统

    • 用某些随机输入来做一些废话魔法

    • 使用网络

    • 使用某种类型的系统调用

    • 从GCC和/或Clang未知的外部库中调用内容

    • 使功能不完全无法使用

    • 从GCC和/或Clang未知的外部库中调用内容

    • 使用 __attribute__ ((noinline))

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

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

相关问题