C++编译器在处理指针时能够优化掉代码吗?

3
有了这两个问题作为背景(firstsecond),我对C++编译器在处理指针时能够进行多少优化感到好奇?具体来说,我对编译器在优化可能永远不会运行的代码时有多聪明感兴趣。
(有人可能会指出这是this question的重复,但这个问题的特定部分并没有完全得到回答。因此,我决定提出一个新问题,只涉及这个问题。)
(我不是C++专家,所以以下陈述可能不正确,但我还是尝试一下)。 C++编译器可能会优化掉它将永远不执行或永远不退出的代码部分(例如循环)。以下是一个例子:

void test() {
    bool escape = false;

    while ( !escape ); // Will never be exited

    // Do something useful after having escaped
}

编译器很可能会认为由于代码从未更改escape的值,因此循环永远不会退出。这使得循环变得无用。
现在,如果我们将变量更改为指针,编译器是否仍会优化掉循环?假设代码如下:

void test( bool* escape ) {
    while ( *escape ); // Will this be executed?

    // Do something useful after having escaped
}

我的猜测是编译器将取消循环,否则关键字volatile将是多余的,对吗?但是在使用线程时怎么办——它实际上被修改了,但是在函数之外,甚至可能完全在C++文件之外——编译器仍然会删除循环吗?如果由escape指向的变量是全局变量或另一个函数内部的局部变量,是否有区别?编译器能检测到这一点吗?在this question中,有些人说如果在循环内调用库函数,编译器将不会优化循环。那么在使用库函数时有哪些机制可以防止此优化?

编译器无法对已编译为库的函数执行内联或静态分析。因此,它必须假定(我相信)任何这样的函数可能具有副作用,在这种情况下,它无法优化掉对它们的任何调用。 - Oliver Charlesworth
5个回答

9
在第一种情况下(while ( !escape );),编译器将把它视为label: goto label;并省略其后的所有内容(可能会给出警告)。
在第二种情况下(while ( *escape );),编译器无法知道运行时*escape是true还是false,因此必须进行比较和循环。但请注意,它只需要从*escape中读取一次值,也就是说,它可以将其视为:
 bool b = *escape;
 label:   if (b) goto label;

volatile 会强制每次循环都从 '*escape' 中读取值。


在第二种情况下,如果另一个线程修改了相同的内存,*escape 可能会发生变化。 因此,编译器不应该每次计算该值吗? - Jimmy
@Jimmy 如果*escape可以从另一个线程进行修改,那么在没有某种形式的同步的情况下访问它是不正确的(部分原因是这个)。如果*escape可以通过外部硬件事件进行修改(例如映射内存),那么应该将其标记为volatile以确保编译器每次都会重新读取它。 - Tyler McHenry
@Jimmy:不,Escape不能被另一个线程修改---如果没有将其标记为“volatile”,则表示您承诺另一个线程不会更改它。该关键字是有目的的。请注意,如果它被写成while(*escape) DoSomething();,即使没有volatile,它也会被重新加载,因为escape可能在DoSomething()中被修改。 - James Curran
volatile关键字是否可以继承?例如,如果我有以下代码: void test(bool* foo) { while(*foo); }void test2(bool* bar) { for(;*foo;); } 这会如何处理?如果我从多个线程调用它们并传递一个volatile bool*,编译器是否知道正确处理它?更重要的是,如果这两种方法在与调用代码分开编译的库中,该怎么办?抱歉我这么啰嗦,但我很好奇这个责任被传递给程序员的程度... - Jimmy

2

请记住,C ++编译器不知道也不关心你的线程。Volatile是你所拥有的唯一保障。任何编译器都可以进行优化,破坏多线程代码但单线程上运行良好,这是完全合法的。当讨论编译器优化时,请摒弃线程,因为它并不在考虑范围内。

现在,让我们来谈谈库函数。当然,任何一个库函数都可以随时更改你的函数中的*escape值,因为编译器无法知道它们如何工作。如果你将函数传递为回调函数,情况就尤其如此。但是,如果你有一个包含源代码的库,编译器可以深入挖掘并发现*escape在其中没有被修改。

当然,如果循环为空,它几乎肯定会让你的程序挂起,除非它能确定该条件在开始时不为真。删除空的无限循环不是编译器的工作,而是程序员自己的脑细胞的工作。


2
这样的问题通常包含一个非常不切实际的代码片段,这是一个通用的问题。"编译器会做什么"这个问题需要真正的代码,因为编译器被设计和优化来编译真正的代码。由于代码没有副作用,大多数编译器将完全消除函数和函数调用,留下一个没有有用答案的问题。
但是,如果你想找到volatile关键字的用途,你可以在SO上找到许多线程,讨论为什么在多线程应用程序中不适合使用volatile。

现在我把示例代码“更加逼真”了,你的答案会是什么? - gablin
1
是的,您的注释将被优化掉 :) - Hans Passant
好的,循环不再被优化掉了。编译器必须考虑第二个代码片段中的指针别名,因此不会对指向的布尔值是否会因doStuff()调用而改变状态做出激烈的结论。除非它被内联,这样它就可以知道。这是代码优化器的一个重要实现细节。 - Hans Passant
我回滚了更改,因为它破坏了问题的整个目的。但还是感谢你的回答。^^ 我觉得我需要更多关于优化的知识,因为这里的一个答案只引出了两三个新问题。 - gablin

2

编译器被允许做的事情与实际编译器所做的事情有所不同。

标准在“1.9程序执行”中进行了说明。标准描述了一种抽象机器,并要求实现具有相同的“可观察行为”。(这就是文末脚注中记录的“as-if”规则。)

来自1.9(6):“抽象机器的可观察行为是其对volatile数据的读写序列和对库I/O函数的调用。” 这意味着,如果您可以证明对函数的修改既不会在该函数内部引起变化,也不会在调用之后引起变化,则该修改是合法的。

从技术上讲,这意味着如果您编写一个永远运行的函数来测试(例如)哥德巴赫猜想,即所有大于2的偶数都是两个质数之和,只有找到一个不正确的才停止,那么足够巧妙的编译器可以替换输出语句或无限循环,具体取决于猜想是错误的还是正确的(或者在哥德尔意义下是不可证明的)。实际上,在编译器拥有优于最好的数学家的定理证明器之前,这将需要一段时间,如果有可能的话。


1

是的,有些编译器比其他编译器更聪明。您的第一个示例是一个不错的编译器,无论是否经过优化,它都会看到它什么也没做,它可能会生成代码,也可能不会警告您的代码什么也没做。

我曾经见过一种编译器,它优化了在循环中调用的不同函数的许多行代码。实际上,我正在尝试进行编译器比较,使用一个在循环中重复调用的lfsr随机器(循环运行了硬编码次数)。一个编译器忠实地编译(和优化)了代码,执行每个功能步骤,而另一个编译器则找出了我在做什么,并且汇编产生的相当于ldr r0,#0x12345,其中0x12345是如果您计算所有变量与循环运行的次数一样多的答案。只有一条指令。

从你的另一个问题中可以看出,你在使用volatile以及语言的其他细节方面存在困难。在你的第二个例子中,只有对该函数的可见性,编译器不知道所指向的内存位置是什么,它很可能是预期在某个时刻会更改的硬件寄存器,或者是被中断或其他线程共享的内存位置,预期在某个时刻会更改。没有volatile,优化器完全有权执行类似于以下的操作:

  ldr r0,[r1]
compare:
  cmp r0,#0
  bne compare

这正是我在学习volatile时所学到的教训。读取了内存位置(一次),然后循环等待该值发生变化,就像我的代码“告诉”它要做的那样。但这并不是我“想要”它做的事情(当指向的寄存器发生变化时退出循环)。

现在,如果你做了这样的事情:

void test( bool* escape ) {
    while ( *escape );
}

pretest() {
    bool escape = false;
    test(&escape);
}

有些编译器会忠实地编译这段代码,即使它什么也不做(除了烧掉时钟周期,这可能正是所需的)。有些编译器可以查看一个函数之外的内容,并看到 while(*escape); 永远不会为真。其中一些编译器将不会在 pretest() 中放置任何代码,但出于某种原因,将忠实地包含 test() 函数在二进制文件中,即使它从未被任何代码调用。有些编译器将完全删除 test() 并将 pretest 保留为简单的返回。所有这些都在这些非现实世界教育示例中完全有效。

底线是你的两个示例完全不同,在第一种情况下,编译器需要知道确定 while 循环是 nop 的所有内容都在那里。在第二种情况下,编译器无法知道 escape 的状态或它是否会改变,并且必须忠实地将 while 循环编译成可执行指令。唯一的优化机会是它是否在每次通过循环时读取内存。

如果您想真正了解发生了什么,请使用不同的优化选项编译这些函数,然后反汇编对象代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接