C++ 创建一个始终返回零的函数,但优化器不会';我不知道
我想创建一个总是返回零的函数,但这一事实对于优化器来说并不明显,因此使用该值的后续计算不会因为“已知零”状态而折叠 在没有链接时间优化的情况下,这通常与将其放入自己的编译单元一样简单:C++ 创建一个始终返回零的函数,但优化器不会';我不知道,c++,performance,optimization,compiler-optimization,C++,Performance,Optimization,Compiler Optimization,我想创建一个总是返回零的函数,但这一事实对于优化器来说并不明显,因此使用该值的后续计算不会因为“已知零”状态而折叠 在没有链接时间优化的情况下,这通常与将其放入自己的编译单元一样简单: int zero() { return 0; } 优化器无法跨单元查看,因此不会发现此函数的始终为零的特性 然而,我需要一些能与LTO一起工作的东西,以及未来尽可能多的智能优化。我考虑从一个全球性的角度阅读: int x; int zero() { return x; } 。。。但在我看来,一个足够
int zero() {
return 0;
}
优化器无法跨单元查看,因此不会发现此函数的始终为零的特性
然而,我需要一些能与LTO一起工作的东西,以及未来尽可能多的智能优化。我考虑从一个全球性的角度阅读:
int x;
int zero() {
return x;
}
。。。但在我看来,一个足够聪明的编译器可能会注意到,x
从未写入,并且仍然决定zero()
始终为零
我考虑过使用volatile
,比如:
int zero() {
volatile int x = 0;
return x;
}
。。。但是volatile读取所需的副作用的实际语义并不完全清楚,并且似乎不排除函数仍然返回零的可能性
这种始终为零但不在编译时的值在几种情况下非常有用,例如强制两个值之间存在无操作依赖关系。类似于:a+=b&zero()
会导致a
依赖于最后一个二进制中的b
,但不会更改a
的值
不要告诉我“标准不保证任何方法可以做到这一点”——我很清楚,我正在寻找一个实用的答案,而不是标准中的语言。如果编译器能够解决这个问题,我会感到惊讶:
int not_a_zero_honest_guv()
{
// static makes sure the initialization code only gets called once
static int const i = std::ifstream("") ? 1:0;
return i;
}
int main()
{
std::cout << not_a_zero_honest_guv();
}
int not_a_zero_money_guv()
{
//static确保只调用一次初始化代码
静态int const i=std::ifstream(“”)1:0;
返回i;
}
int main()
{
std::cout您会发现每个编译器都有一个扩展来实现这一点
通用条款:
MSVC:
在clang和gcc上,重击变量是可行的,但会带来一些开销
int zero()
{
int i = 0;
asm volatile(""::"g"(&i):"memory");
return i;
}
在gcc上的O3下编译为
mov DWORD PTR [rsp-4], 0
lea rax, [rsp-4]
mov eax, DWORD PTR [rsp-4]
ret
叮当作响
mov dword ptr [rsp - 12], 0
lea rax, [rsp - 12]
mov qword ptr [rsp - 8], rax
mov eax, dword ptr [rsp - 12]
ret
.首先,我认为OP的第三个建议:
int zero() {
volatile int x = 0;
return x;
}
事实上会起作用(但这不是我的答案;请参见下文)。两周前,这个完全相同的功能曾是讨论的主题,有很多讨论和不同的观点,我将不在这里重复。但对于最近的测试,请参见
我的答案是在上面添加一个static
,即:
int zero() {
static volatile int x;
return x;
}
请参阅此处的一些测试:
现在,随着
静态的加入,“可观察行为”的抽象概念变得更加可信。只需一点工作,我就可以找出x
的地址,特别是如果我禁用了。这可能在.bss
段中。然后,再多做一点工作,我就可以将调试器/黑客工具附加到正在运行的进程中,然后更改x
的值,并使用volatile
>,我已经告诉编译器我可能会这样做,所以不允许通过优化x
来改变这种“可观察的行为”(它可能通过内联优化zero
的调用,但我不在乎)
的标题有点误导,因为讨论集中在堆栈上的x
而不是局部变量。因此不适用于此处。但我们可以将x
从局部范围更改为文件范围,甚至全局范围,如:
volatile int x;
int zero() {
return x;
}
这不会改变我的论点
进一步讨论: 是的,
volatile
有时会出现问题:例如,请参阅此处和中显示的指向volatile问题的指针
是的,有时(总是?)编译器会有bug
但我想说的是,这个解决方案并不是一个边缘案例,编译器编写人员之间存在共识,我将通过查看现有的分析来做到这一点
John Regehr在2010年的blogpost中报告了一个bug,其中在gcc和Clang中对易失性访问进行了优化(在三个小时内修复)。一位评论员引用了该标准(重点补充):
“6.7.3…构成对具有易失性限定类型的对象的访问的是实现定义的。”
Regehr对此表示同意,但补充道,对于如何处理非边缘案件,各方已达成共识:
是的,对易失性变量的访问是由定义实现的。但是,您错过了一个事实,即所有合理的C实现都将从易失性变量读取为读访问,并将写入易失性变量写入写访问。 有关更多参考资料,请参阅:
- 另一个Regehr 2010博客帖子
- 温特穆特的
这些都是关于编译器错误和程序员错误的报告。但它们表明了
volatile
应该/确实起作用,并且这个答案符合这些规范。这并不能保证,而且实际上在clang
今天已经不起作用了(尽管clang通常尊重noinline
),而gcc已经进行了可能会破坏这一点的过程间分析,尽管它还没有这样做。@BeeOnRope您是对的。这可能应该被报告为错误。此属性是否也具有函数结果不能用于优化的语义?在gcc手册中,它只描述了它不能执行的行为例如,如果(!zero())foo();
,编译器可以(据我所知)将其翻译为zero();foo();
attribute@M.M有趣。gcc和msvc都不这样做。rand()!=0
也可能是相同的……几乎总是零,但编译器肯定不能认为这是一个好的,尽管依赖文件系统行为有点丢脸。只是好奇,为什么要包装ifstream
调用
int zero() {
static volatile int x;
return x;
}
volatile int x;
int zero() {
return x;
}