gcc - 添加两个 float

标签 gcc floating-point clang c99 fenv

我想计算两个 IEEE 754 binary64 数字的总和,四舍五入。为此,我编写了下面的 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 版本 4.2.1(Apple Inc. build 5664)
$ gcc -Wall -std=c99 add.c && ./a.out
add.c:3:警告:忽略#pragma STDC FENV_ACCESS
0x1p+0
$ 铿锵 -v
苹果 clang 版本 1.5 (tags/Apple/clang-60)
目标:x86_64-apple-darwin10
线程模型:posix
$ clang -Wall -std=c99 add.c && ./a.out
add.c:3:14:警告:不支持编译指示 STDC FENV_ACCESS ON,忽略
pragma [-Wunknown-pragmas]
#pragma STDC FENV_ACCESS ON
^
生成 1 个警告。
0x1p+0

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

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

$ clang -Wall -std=c99 -S add.c && cat add.s
add.c:3:14:警告:不支持编译指示 STDC FENV_ACCESS ON,忽略
pragma [-Wunknown-pragmas]
#pragma STDC FENV_ACCESS ON
^
生成 1 个警告。

LCPI1_0:
.quad 4607182418800017408

callq _fesetround
移动 $1, %cl
movsd LCPI1_0(%rip), %xmm0
leaq L_.str(%rip), %rdx
movq %rdx, %rdi
移动 %cl, %al
callq _printf

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

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

我的问题是:我应该在 GCC 或 Clang 中使用什么命令行选项来编译包含旨在以 FPU 舍入模式而不是默认模式执行的代码的 C99 编译单元?

题外话

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

浮点数 | |
环境访问 |不适用 |库功能,无需编译器支持。
| |

最佳答案

我找不到任何可以满足您需求的命令行选项。但是,我确实找到了一种重写代码的方法,这样即使进行了最大优化(甚至是架构优化),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)编译代码时,他们会尝试查找和替换可以在运行时计算的值。这被称为 持续传播 .如果您只是简单地编写了另一个函数,则不会发生持续传播,因为它不应该跨函数。

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

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

现在,如果一个函数是完全可内联的(这意味着它不会对外部库进行任何调用,不包括 GCC 和 Clang 中包含的一些默认值 - libclibm 等)并且是纯函数,那么它们将适用不断传播到函数。

换句话说,如果我们不希望它们通过函数调用传播常量,我们可以做以下两件事之一:
  • 使函数显得不纯:
  • 使用文件系统
  • 用来自某处的随机输入做一些废话魔术
  • 使用网络
  • 使用某种系统调用
  • 从 GCC 和/或 Clang 未知的外部库调用某些内容
  • 使函数不完全可内联
  • 从 GCC 和/或 Clang 未知的外部库调用某些内容
  • 使用__attribute__ ((noinline))

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

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

    关于gcc - 添加两个 float ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24541589/

    相关文章:

    c - 使用 GCC 链接对象的两种方法?

    c++ - LLVM cpp 后端,它会取代 c 后端吗?

    c - 将两个 float 相乘并不能给出准确的结果

    floating-point - 您将使用什么计量单位来存储工程数据?

    python - 将字符串解析为具有不同分隔符的 float

    c - fscanf != eof Clang 编译器,异常行为

    compiler-construction - 将 LLVM/CLANG 用于自定义字节码 VM 的程序的大小是多少?

    c++ - 使用别名引用匿名结构会导致错误

    mysql - 意外的函数调用

    c++ - 将库与 minGW 链接