Добавление двух чисел с плавающей точкой
Я хотел бы вычислить сумму округленных в большую сторону двух двоичных чисел 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.)