我找不到任何可以满足您需求的命令行选项。但是,我确实找到了一种重写代码的方法,这样即使进行了最大优化(甚至是架构优化),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
(上帝螺栓 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
(上帝螺栓 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))
,然后调用它。
当它被编译时,它们不会违反内联规则,并且通过扩展,不会违反常量传播规则,因此,该值将在运行时使用适当的舍入模式集进行计算。