goto
使用现代 C++ 编译器有哪些性能优势或劣势?
我正在编写一个 C++ 代码生成器,使用goto
它会更容易编写。没有人会触及生成的 C++ 文件,所以不要对我说“goto is bad”。作为一个好处,它们节省了临时变量的使用。
我想知道,从纯粹的编译器优化的角度来看,goto 对编译器优化器的结果是什么?与使用临时/标志相比,它是否使代码更快、更慢或性能通常没有变化。
goto
使用现代 C++ 编译器有哪些性能优势或劣势?
我正在编写一个 C++ 代码生成器,使用goto
它会更容易编写。没有人会触及生成的 C++ 文件,所以不要对我说“goto is bad”。作为一个好处,它们节省了临时变量的使用。
我想知道,从纯粹的编译器优化的角度来看,goto 对编译器优化器的结果是什么?与使用临时/标志相比,它是否使代码更快、更慢或性能通常没有变化。
将受到影响的编译器部分与流程图一起使用。只要您编写严格可移植的代码,用于创建特定流程图的语法通常是无关紧要的——如果您使用 a而不是实际语句创建类似while
循环的东西,它不会产生相同的流程图就好像您使用循环的语法一样。然而,使用不可移植的代码,现代编译器允许您向循环添加注释以预测它们是否会被采用。根据编译器,您可能会或可能无法使用 a 复制该额外信息(但大多数具有循环注释的循环也具有语句注释,因此 a或在控制 agoto
while
while
goto
if
likely taken
likely not taken
if
goto
通常与相应循环上的类似注释具有相同的效果)。
但是,可以生成一个带有goto
s 的流图,这是任何正常的流控制语句(循环、开关等)都无法生成的,例如有条件地直接跳转到循环的中间,具体取决于一个全球性的。在这种情况下,您可能会生成一个不可约的流程图,并且当/如果您这样做时,这通常会限制编译器优化代码的能力。
换句话说,如果(例如)您将使用 normal for
, while
,switch
等编写的代码转换为goto
在每种情况下都可以使用,但保留相同的结构,则几乎任何合理的现代编译器都可能生成基本相同的代码方法。但是,如果您使用goto
s 来产生像我几十年前不得不看的一些 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 在编译器的优化器上的结果是什么?与使用临时/标志相比,它是否使代码更快、更慢或性能通常没有变化。
你为什么在乎?您最关心的应该是让您的代码生成器创建正确的代码。效率远不如正确性重要。您的问题应该是“我对 goto 的使用会使我生成的代码更可能还是不太可能是正确的?”
查看 lex/yacc 或 flex/bison 生成的代码。该代码充满了goto。这是有充分理由的。lex 和 yacc 实现有限状态机。由于机器在状态转换时会进入另一个状态,因此 goto 可以说是这种转换最自然的工具。
在许多情况下,有一种简单的方法可以通过在语句while
周围使用循环来消除这些 goto。switch
这是结构化代码。Per Douglas Jones(Jones DW,如何(不)编写有限状态机,SIGPLAN Not. 23, 8 (Aug. 1988), 19-22.),这是对 FSM 进行编码的最糟糕的方法。他认为基于 goto 的方案更好。
他还认为还有一种更好的方法,即使用图论技术将 FSM 转换为控制流程图。这并不总是那么容易。这是一个NP难题。这就是为什么您仍然会看到很多 FSM,尤其是自动生成的 FSM,它们被实现为围绕开关的循环或通过 goto 实现的状态转换。
我非常同意 David Hammen 的回答,但我只有一点要补充。
当人们被教导编译器时,他们会被教导编译器可以做的所有美妙的优化。
他们没有被告知这的实际价值取决于用户是谁。
如果您正在编写(或生成)和编译的代码包含很少的函数调用,并且本身可能会占用其他程序的大部分时间,那么是的,编译器优化很重要。
如果正在生成的代码包含函数调用,或者由于某些其他原因,程序计数器在生成的代码中花费了一小部分时间,则不必担心。为什么?因为即使该代码可以如此积极地优化以至于它花费了零时间,它所节省的时间也不会超过那一小部分,而且可能还有更大的性能问题,编译器无法修复,很高兴能避开你的注意力。