goto 对C++编译器优化的影响

effect of goto on C++ compiler optimization

本文关键字:影响 优化 编译器 C++ goto      更新时间:2023-10-16

goto与现代C++编译器一起使用的性能优势或惩罚是什么?

我正在编写一个C++代码生成器,使用 goto 将使编写更容易。没有人会碰生成的C++文件,所以不要把所有的"goto 不好"都放在我身上。好处是,它们节省了临时变量的使用。

我想知道,从纯粹的编译器优化的角度来看,goto 对编译器的优化器有什么结果?与使用临时/标志相比,它是否使代码更快更慢或通常没有性能变化

编译器中受影响的部分与流图一起工作。只要您编写严格可移植的代码,用于创建特定流图的语法通常无关紧要 - 如果您使用 goto 而不是实际的 while 语句创建类似 while 循环的东西,它不会生成与使用while循环语法相同的流图。但是,使用非可移植代码,现代编译器允许您向循环添加注释,以预测它们是否会被采用。根据编译器的不同,您可能能够也可能无法使用goto复制该额外信息(但大多数具有循环注释的语句也有if语句的注释,因此控制gotoif上的likely takenlikely not taken通常与相应循环上的类似注释具有相同的效果(。

但是,可以生成一个具有goto的流图,而这些流图无法由任何正常的流控制语句(循环、开关等(生成,例如有条件地直接跳转到循环的中间,具体取决于全局中的值。在这种情况下,您可能会生成一个不可约的流图,当您这样做时/如果这样做,这通常会限制编译器优化代码的能力。

换句话说,如果(例如(你采用用普通forwhileswitch等编写的代码,并将其转换为在每种情况下都使用goto,但保留相同的结构,几乎任何相当现代的编译器都可能以任何一种方式生成基本相同的代码。但是,如果您使用goto来产生混乱的意大利面条,就像我几十年前不得不查看的一些FORTRAN一样,那么编译器可能无法用它做太多事情。

您认为循环在程序集级别是如何表示的?

使用跳转指令来标记...

许多编译器实际上甚至在他们的中间表示中使用跳转:

int loop(int* i) {
  int result = 0;
  while(*i) {
    result += *i;
  }
  return result;
}
int jump(int* i) {
  int result = 0;
  while (true) {
    if (not *i) { goto end; }
    result += *i;
  }
end:
  return result;
}

LLVM 中的收益率:

define i32 @_Z4loopPi(i32* nocapture %i) nounwind uwtable readonly {
  %1 = load i32* %i, align 4, !tbaa !0
  %2 = icmp eq i32 %1, 0
  br i1 %2, label %3, label %.lr.ph..lr.ph.split_crit_edge
.lr.ph..lr.ph.split_crit_edge:                    ; preds = %.lr.ph..lr.ph.split_crit_edge, %0
  br label %.lr.ph..lr.ph.split_crit_edge
; <label>:3                                       ; preds = %0
  ret i32 0
}
define i32 @_Z4jumpPi(i32* nocapture %i) nounwind uwtable readonly {
  %1 = load i32* %i, align 4, !tbaa !0
  %2 = icmp eq i32 %1, 0
  br i1 %2, label %3, label %.lr.ph..lr.ph.split_crit_edge
.lr.ph..lr.ph.split_crit_edge:                    ; preds = %.lr.ph..lr.ph.split_crit_edge, %0
  br label %.lr.ph..lr.ph.split_crit_edge
; <label>:3                                       ; preds = %0
  ret i32 0
}

其中br分支指令(条件跳转(。

所有优化都在此结构上执行。因此,goto是优化器的面包和黄油。

我想知道,从纯粹的编译器优化来看,goto对编译器的优化器的结果是什么?与使用临时/标志相比,它是否使代码更快、更慢或通常没有性能变化。

你为什么关心?您主要关心的应该是让代码生成器创建正确的代码。效率远不如正确性重要。你的问题应该是"我对gotos的使用是否会使我生成的代码更有可能或更不可能是正确的?

看看 lex/yacc 或 flex/bison 生成的代码。该代码充满了gotos。这是有充分理由的。lex 和 yacc 实现了有限状态机。由于机器在状态转换时会进入另一种状态,因此 goto 可以说是这种转换最自然的工具。

在许多情况下,有一种简单的方法可以通过在switch语句周围使用while循环来消除这些 goto。这是结构化代码。根据Douglas Jones(Jones D. W.,如何(不(编码有限状态机,SIGPLAN Not.23,8(Aug.1988(,19-22.(,这是编码FSM的最糟糕的方法。他认为基于goto的方案更好。

他还认为,还有一种更好的方法,即使用图论技术将FSM转换为控制流图。这并不总是那么容易。这是一个NP难题。这就是为什么你仍然看到很多FSM,特别是自动生成的FSM,要么是围绕开关的循环,要么是通过gotos实现的状态转换。

我衷心同意David Hammen的回答,但我只有一点要补充。

当人们学习编译器时,他们被教导编译器可以做的所有精彩优化。

他们没有被告知它的实际价值取决于用户是谁。

如果您正在编写(或生成(和编译的代码包含很少的函数调用,并且本身可能会消耗其他程序的大部分时间,那么是的,编译器优化很重要。

如果生成的代码包含函数调用,或者由于某种其他原因,程序计数器在生成的代码中花费了一小部分时间,则不值得担心。为什么?因为即使该代码可以如此积极地优化,以至于花费时间,它也不会节省超过一小部分,并且可能存在编译器无法修复的更大的性能问题,这些问题很乐意逃避您的注意。