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

Я хотел бы вычислить сумму округленных в большую сторону двух двоичных чисел 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 версия 4.2.1 (Apple Inc., сборка 5664)
$ gcc -Wall -std=c99 add.c && ./a.out 
add.c:3: предупреждение: игнорирование #pragma STDC FENV_ACCESS
0x1p+0
$ clang -v
Apple Clang версия 1.5 (теги /Apple/ Clang-60)
Цель: x86_64-apple-darwin10
Модель потока: posix
$ clang -Wall -std=c99 add.c && ./a.out 
add.c:3:14: предупреждение: прагма STDC FENV_ACCESS ON не поддерживается, игнорируется
      прагма [-Wunknown-прагмы]
#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 не поддерживается, игнорируется
      прагма [-Wunknown-прагмы]
#pragma STDC FENV_ACCESS ON
             ^
1 предупреждение сгенерировано....
LCPI1_0:
    .quad   4607182418800017408...
    callq   _fesetround
    Movb    $1, % кл
    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.

с плавающей точкой |     |
доступ к окружающей среде | N/A | Функция библиотеки, поддержка компилятора не требуется.
в  |     |

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) компилируют код, они пытаются найти и заменить значения, которые могут быть вычислены во время выполнения. Это известно как постоянное распространение. Если бы вы просто написали другую функцию, постоянное распространение прекратилось бы, поскольку она не должна пересекать функции.

Однако, если они видят функцию, которую они могли бы теоретически заменить кодом вместо вызова функции, они могут сделать это. Это известно как встраивание функции. Если функция inlining будет работать над функцией, мы говорим, что эта функция (неожиданно) встроена.

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

Теперь, если функция полностью встраиваемая (это означает, что она не выполняет никаких вызовов внешних библиотек, за исключением нескольких значений по умолчанию, включенных в GCC и Clang) - libc, libmи т. д.) и будет чистым, тогда они будут применять постоянное распространение к функции.

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

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

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

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

Clang или gcc -frounding-mathсообщает им, что код может работать с нестандартным режимом округления. Это не совсем безопасно (предполагается, что один и тот же режим округления активен все время), но лучше, чем ничего. Возможно, вам все равно придется использовать 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; на самом деле происходит то, что GCC делает asm безопасным, если режим округления уже был установлен на что-то еще до того, как был вызван main.

Но, к сожалению, как отмечается в вики, GCC по-прежнему предполагает, что один и тот же режим округления действует везде ( ошибка GCC № 34678). Это означает, что она будет CSE два расчета тех же входов до / после вызова fesetround, потому что это не лечит 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;
}

компилируется следующим образом (Godbolt) с gcc10.2 (и по сути то же самое с clang10.1). Также включает ваш main, что делает asm, который вы хотите.

       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

Это та же проблема, о которой @Marc Glisse предупреждал в комментариях под другим ответом, если ваша функция noinline выполнила ту же математику до и после изменения режима округления.

(И отчасти удача, что GCC решил не делать математику перед вызовом fesetroundв первый раз, поэтому ему нужно будет только передать результат вместо обоих входов. x86-64 System V не имеет никаких сохраненных при вызове регистров XMM.)

Другие вопросы по тегам