创建一个始终返回零但优化器不知道的函数

Create a function that always returns zero, but the optimizer doesn't know

本文关键字:优化 不知道 函数 返回 一个 创建      更新时间:2023-10-16

我想创建一个总是返回零的函数,但这一事实对优化器来说不应该是显而易见的,这样使用该值的后续计算就不会因为"已知零"状态而不断减少。

在没有链接时间优化的情况下,这通常就像把它放在自己的编译单元中一样简单:

int zero() {
return 0;
}

优化器无法跨单元查看,因此不会发现此函数的始终为零的特性。

然而,我需要一些能与LTO一起工作的东西,以及未来尽可能多的巧妙优化。我考虑过从一个全球性的:

int x;
int zero() {
return x;
}

但在我看来,一个足够聪明的编译器可以注意到CCD_ 1从未被写入,并且仍然决定CCD_。

我考虑过使用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();
}

这使用了一个复杂的(不可预测的)本地静态函数的运行时初始化。如果顽皮的小编译器发现一个空文件名总是会失败,那么就在里面放一些非法的文件名。

首先说一句:我相信OP的第三个建议:

int zero() {
volatile int x = 0;
return x;
}

事实上是可行的(但这不是我的答案;见下文)。两周前,这个完全相同的函数是《允许编译器优化掉局部易失性变量吗?》的主题?,有很多讨论和不同意见,我在此不再赘述。但要了解最近对此的测试,请参阅https://godbolt.org/g/SA7k5P.


我的答案是在上面添加一个static,即:

int zero() {
static volatile int x;
return x;
}

请在此处查看一些测试:https://godbolt.org/g/qzWYJt.

现在,随着static的加入,"可观察行为"的抽象概念变得更加可信。只要做一点工作,我就可以算出x0的地址,特别是如果我禁用了地址空间布局随机化。这可能在.bss段中。然后,再做一点工作,我就可以在运行的进程中附加一个调试器/黑客工具,然后更改x的值。对于volatile,我已经告诉编译器我可能会这样做,所以不允许通过优化x来改变这种"可观察的行为"。(它也许可以通过内联来优化对zero调用,但我不在乎。)

标题是"允许编译器优化掉局部易失性变量吗?"?这有点误导,因为讨论集中在x位于堆栈上,而不是局部变量。所以这里不适用。但我们可以将x从本地范围更改为文件范围,甚至全局范围,如:

volatile int x;
int zero() {
return x;
}

这不会改变我的论点。


进一步讨论:

是的,volatile有时会有问题:例如,请参阅此处显示的指向易失性问题的指针https://godbolt.org/g/s6JhpL以及在通过易失性引用/指针访问声明的非易失性对象是否将易失性规则赋予所述访问?。

是的,有时(总是?)编译器会有错误。

但我想说的是,这个解决方案不是一个边缘案例,编译器作者之间达成了共识,我将通过查看现有的分析来做到这一点。

John Regehr在2010年的博客文章《Volatile Structs Are Broken》中报告了一个漏洞,其中在gcc和Clang中都优化了Volatile访问。(它在三个小时内就被固定了。)一位评论员引用了标准(增加了重点):

"6.7.3…对具有volatile限定类型的对象的访问是由实现定义的。">

Regehr表示同意,但补充说,在如何处理非边缘情况方面达成了共识:

是的,对易失性变量的访问是由实现定义的。但是,您忽略了这样一个事实,即所有合理的C实现都将从易失性变量读取视为读取访问,将对易失性可变变量的写入视为写入访问。

获取更多参考。参见:

  • E。Eide,J.Regehr,"Volatiles Are Miscompiled,and What to Do It",第八届ACM和IEEE嵌入式软件国际会议论文集,2008。

  • Regehr 2010年的另一篇博客文章,使用volatile破解系统代码的九种方法。

  • 温特穆特对挥发性及其有害影响的回答。

这些是关于编译器错误和程序员错误的报告。但他们展示了volatile应该如何工作,并且这个答案符合这些规范。

您会发现每个编译器都有一个扩展来实现这一点。

GCC:

__attribute__((noinline))
int zero()
{
return 0;
}

MSVC:

__declspec(noinline)
int zero()
{
return 0;
}

在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

Live。

相关文章: