Ответ 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))
, а затем вызвать его.
Когда он скомпилирован, они не будут нарушать вложение и, как правило, постоянные правила распространения, и, следовательно, значение будет вычислено во время выполнения с соответствующим режимом округления.