Добавление двух чисел с плавающей запятой

Я хотел бы вычислить сумму, округленную, из двух двоичных чисел IEEE 754. С этой целью я написал программу 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, содержащую запись ниже, что я просто оставлю здесь, если кто-то другой находит это забавным. Grrrr.

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

Ответы

Ответ 1

Я не мог найти никаких параметров командной строки, которые бы сделали то, что вы хотели. Однако я нашел способ переписать ваш код, чтобы даже при максимальной оптимизации (даже при оптимизации архитектуры) ни 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 - libc, libm и т.д.), и является чистым, тогда они будут применять постоянное распространение функции.

Другими словами, если мы не хотим, чтобы они распространяли константы через вызов функции, мы можем сделать одну из двух вещей:

  • Сделать функцию нечистой:
    • Использование файловой системы
    • Сделайте какую-нибудь чушь с каким-то случайным входом откуда-то
    • Использовать сеть
    • Используйте некоторый системный вызов какого-либо типа
    • Вызвать что-нибудь из внешней библиотеки, неизвестной GCC и/или Clang
  • Сделать функцию не полностью встроенной
    • Вызвать что-нибудь из внешней библиотеки, неизвестной GCC и/или Clang
    • Используйте __attribute__ ((noinline))

Теперь этот последний самый простой. Как вы могли догадаться, __attribute__ ((noinline)) отмечает, что функция является неотъемлемой. Поскольку мы можем воспользоваться этим, все, что нам нужно сделать, это сделать еще одну функцию, которая делает любое вычисление, которое мы хотим, пометить его с помощью __attribute__ ((noinline)), а затем вызвать его.

Когда он скомпилирован, они не будут нарушать вложение и, как правило, постоянные правила распространения, и, следовательно, значение будет вычислено во время выполнения с соответствующим режимом округления.