繁体   English   中英

在处理指针时,C ++编译器能否优化代码?

[英]Can a C++ compiler optimize away code when dealing with pointers?

有了这两个问题作为背景( 第一第二 ),我很好奇C ++编译器在处理指针时可以执行多少优化? 更具体地说,我对编译器在优化它可能检测到的永远不会运行的代码时的智能感兴趣。

(有些人可能会指出,这可能是这个问题的一个骗局,但它的这个特殊部分并没有完全回答。所以我决定开始一个只涉及这个问题的新问题。)

(我不是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指向的变量是全局变量,或者是另一个函数中的局部变量,它会有所不同吗? 编译器可以进行此检测吗? 这个问题中 ,有人说如果在循环内部调用库函数,编译器将不会优化循环。 当使用阻止此优化的库函数时,会有什么机制?

在第一种情况下( while ( !escape ); )编译器会将其视为label: goto label; 并省略其后的所有内容(可能会给你一个警告)。

在第二种情况下( while ( *escape ); ),编译器无法知道* escape在运行时是真还是假,所以它必须执行comaprision和循环。 但是请注意,它只需要从* escape读取一次值,即它可以将其视为:

 bool b = *escape;
 label:   if (b) goto label;

volatile会强制它每次通过循环从'* escape'读取值。

请记住,C ++编译器不知道或给出两个关于你的线程的屎。 挥发性就是你所拥有的。 对于任何编译器来说,进行优化以破坏多线程代码但在单线程上运行良好是完全合法的。 在讨论编译器优化,沟渠线程时,它只是在图片中。

现在,库函数。 当然,您的函数可以随时通过任何这些函数更改* escape,因为编译器无法知道它们是如何工作的。 如果将函数作为回调传递,则尤其如此。 但是,如果你有一个拥有源代码的库,那么编译器可能会深入研究并发现* escape永远不会被改变。

当然,如果循环为空,几乎可以肯定只是让你的程序挂起,除非它可以确定启动时条件不正确。 删除空的无限循环不是编译器的工作,它是程序员脑细胞的工作。

像这样的问题的一般问题是它们通常包含一个非常不切实际的代码片段。 “编译器会做什么”问题需要真正的代码。 由于编译器的设计和优化是为了编译实际代码。 大多数编译器将完全消除函数和函数调用,因为代码没有副作用。 让我们留下一个没有实用答案的问题。

但是,当然,您倾向于寻找volatile关键字的用法。 您可以在SO上找到许多线程,讨论为什么volatile不适合多线程应用程序。

编译器允许执行的操作与实际编译器的功能有所不同。

该标准涵盖了“1.9程序执行”中的内容。 该标准描述了一种抽象机器,并且实现需要提出相同的“可观察行为”。 (这是“假设”规则,如脚注中所述。)

从1.9(6):“抽象机器的可观察行为是它对volatile数据的读写顺序以及对库I / O函数的调用。” 这意味着,如果您可以证明对函数的修改既不会导致对该函数的修改,也不会在该函数调用之后,则修改是合法的。

从技术上讲,这意味着如果你编写一个将永远运行的函数测试(比方说)Goldbach的猜想,所有大于2的偶数都是两个素数的总和,并且只有在找到一个非素数时才停止,一个足够巧妙的编译器可以替换输出语句或无限循环,具体取决于猜测是假还是真(或者在Goedel意义上是无法证明的)。 在实践中,在编译器具有比最佳数学家更好的定理证明之前,将会有一段时间。

是的,有些编译器比其他编译器更聪明。 你的第一个例子是一个体面的编译器,无论是否优化都会看到它什么都不做,它可能会也可能不会为它生成代码,它可能会或可能不会警告你,你的代码什么都不做。

我见过一个编译器,它优化了循环中调用的不同函数中的许多代码行。 我试图实际进行编译器比较,在循环中重复调用lfsr随机函数(循环运行硬编码次数)。 一个编译器忠实地编译(和优化)代码,执行每个功能步骤,另一个编译器弄清楚我正在做什么和生成的汇编程序相当于ldr r0,#0x12345其中0x12345是答案,如果你计算所有变量为循环运行很多次。 一条指令。

正如您在其他一个问题中所看到的那样,您正在努力使用该语言的volatile和其他细节。 在你的第二个例子中,只有对该函数的可见性,编译器不知道所指向的内存位置是什么,它很可能是一个预期在某个时刻发生变化的硬件寄存器,或者是一个共享的内存位置。中断或其他线程的某种其他方式,预计会在某些时候发生变化。 如果没有易变性,优化器完全有权做这样的事情:

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

正是我在学习挥发性知识时学到的教训。 读取(一次)内存位置,然后循环等待该值更改,就像我的代码“告诉”它要做的那样。 不是我“想要”它做的事情(当寄存器被指向改变时退出循环)。

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

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

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

有些编译器会忠实地编译这段代码,即使它什么都不做(除了刻录时钟周期,这可能正是所期望的)。 有些编译器可以在一个函数外面查看另一个函数并看到while(* escape); 从来都不是真的。 其中一些不会在pretest()中放入任何代码,但由于某种原因,它会忠实地在二进制文件中包含test()函数,即使它从未被任何代码调用过。 一些编译器将完全删除test()并将pretest作为简单的返回。 在这些非现实世界教育的例子中完全有效。

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

如果您想真正了解正在发生的事情,请使用不同的优化选项编译这些函数,然后反汇编目标代码。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM