使用
goto
的循环
int i=3;
loop:
printf("something");
if(--i) goto loop;
while循环
int i=3;
while(i--) {
printf("something");
}
for
循环
for(int i=3; i; i--) {
printf("something");
}
使用 goto
的循环
int i=3;
loop:
printf("something");
if(--i) goto loop;
while循环
int i=3;
while(i--) {
printf("something");
}
for
循环
for(int i=3; i; i--) {
printf("something");
}
一般来说,for
和while
循环会被编译成与goto
相同的代码,所以通常不会有区别。如果你怀疑,可以尝试使用这三个方法,并查看哪个需要更长时间。即使你循环了十亿次,很可能也无法测量出差异。
如果你查看这个答案,你会看到编译器可以为for
、while
和goto
生成完全相同的代码(只是在这种情况下没有条件)。
goto
版本可能会被翻译为一个有条件测试后跟着一个无条件分支,而不是直接的有条件分支。 - Clifford编写简短的程序,然后执行以下步骤:
gcc -S -O2 p1.c
gcc -S -O2 p2.c
gcc -S -O2 p3.c
-O3
会给你完全的优化。 - Andrew-flto
- user426这可能与编译器、优化器和架构有关。
例如,代码if(--i) goto loop;
是一个条件测试后面跟着一个无条件分支。编译器可能只会生成相应的代码,或者它可能足够聪明(尽管一个没有至少这么多智慧的编译器可能不值得一提),生成一个单一的条件分支指令。另一方面,while(i--)
在源级别上已经是一个条件分支,因此无论编译器实现或优化器的复杂程度如何,将其转换为机器级别的条件分支可能更容易。
最终,差异可能微乎其微,只有在需要大量迭代时才相关,并且你应该回答这个问题的方法是为特定目标和编译器(以及编译器设置)构建代码,然后检查生成的机器级别代码或直接测量执行时间。
在你的示例中,循环中的printf()将在任何情况下都占据主导地位;在循环中使用更简单的东西将使观察到的差异更容易。我建议使用一个空循环,然后声明i
为volatile
,以防止循环被优化成空。
for
, while
等方式。goto
可以获得一些优势,但通常只有在您生成的控制流不太规范(至少不是很干净)时才能发挥作用。一个典型的例子是跳到循环中间以获得一个半循环结构,大多数语言的普通循环语句(包括 C 语言的)都不能提供这种结构。所有循环和goto之间不应该有任何显著差异。除了一个想法,编译器更可能根本不尝试优化GOTO事物。
而且,试图优化循环中由编译器生成的内容并没有太多意义。更有意义的是优化循环内部的代码,或者减少迭代次数等。
g++ -S -O3 filename.cpp
clang++ -S -O3 filename.cpp
,还有一些汇编注释,你将在下面看到,以帮助我理解。)首先,我比较了label:
和goto
与do {} while
。你不能(诚实地)将for () {}
循环与此进行比较,因为for循环总是首先评估条件。这一次,条件仅在执行循环代码一次后才被评估。
#include <iostream>
void testGoto()
{
__asm("//startTest");
int i = 0;
loop:
std::cout << i;
++i;
if (i < 100)
{
goto loop;
}
__asm("//endTest");
}
#include <iostream>
void testDoWhile()
{
__asm("//startTest");
int i = 0;
do
{
std::cout << i;
++i;
}
while (i < 100);
__asm("//endTest");
}
goto
还是do {} while
,汇编代码都完全相同,根据编译器而定:
g++:
xorl %ebx, %ebx
leaq _ZSt4cout(%rip), %rbp
.p2align 4,,10
.p2align 3
.L2:
movl %ebx, %esi
movq %rbp, %rdi
addl $1, %ebx
call _ZNSolsEi@PLT
cmpl $100, %ebx
jne .L2
clang++:
xorl %ebx, %ebx
.p2align 4, 0x90
.LBB0_1: # =>This Inner Loop Header: Depth=1
movl $_ZSt4cout, %edi
movl %ebx, %esi
callq _ZNSolsEi
addl $1, %ebx
cmpl $100, %ebx
jne .LBB0_1
# %bb.2:
label:
和goto
与while {}
和for () {}
。这一次,在循环代码执行一次之前,条件被评估。goto
,我不得不反转条件,至少在第一次时是如此。我看到了两种实现的方式,所以我尝试了两种方式。#include <iostream>
void testGoto1()
{
__asm("//startTest");
int i = 0;
loop:
if (i >= 100)
{
goto exitLoop;
}
std::cout << i;
++i;
goto loop;
exitLoop:
__asm("//endTest");
}
#include <iostream>
void testGoto2()
{
__asm("//startTest");
int i = 0;
if (i >= 100)
{
goto exitLoop;
}
loop:
std::cout << i;
++i;
if (i < 100)
{
goto loop;
}
exitLoop:
__asm("//endTest");
}
#include <iostream>
void testWhile()
{
__asm("//startTest");
int i = 0;
while (i < 100)
{
std::cout << i;
++i;
}
__asm("//endTest");
}
#include <iostream>
void testFor()
{
__asm("//startTest");
for (int i = 0; i < 100; ++i)
{
std::cout << i;
}
__asm("//endTest");
}
goto
1还是2、while {}
或for () {}
,汇编代码完全相同,编译器也是如此,只有g++存在一些微小的例外,可能是无意义的:
g++:
xorl %ebx, %ebx
leaq _ZSt4cout(%rip), %rbp
.p2align 4,,10
.p2align 3
.L2:
movl %ebx, %esi
movq %rbp, %rdi
addl $1, %ebx
call _ZNSolsEi@PLT
cmpl $100, %ebx
jne .L2
针对g++的异常情况:在goto2汇编的结尾处,添加了以下汇编代码:
.L3:
endbr64
goto
1的汇编代码。不过我认为这完全是无关紧要的。 xorl %ebx, %ebx
.p2align 4, 0x90
.LBB0_1: # =>This Inner Loop Header: Depth=1
movl $_ZSt4cout, %edi
movl %ebx, %esi
callq _ZNSolsEi
addl $1, %ebx
cmpl $100, %ebx
jne .LBB0_1
# %bb.2:
label:
和goto
、do {} while
、while {}
和for () {}
之间似乎没有任何区别。break
和continue
;但是,由于在任何情况下生成的4个汇编代码相同,因此我只能推测break
和continue
也将完全相同,特别是因为汇编正在为每种情况使用标签和跳转。我认为在正常情况下编译器会有一些代码。
事实上,我认为goto有时非常方便,尽管它很难读。
在一些特定的领域中,goto仍然被一些非常聪明的人广泛使用,并且在这些环境中并不存在对goto的偏见。我曾经在一个专注于模拟的公司工作过,那里的所有本地fortran代码都有大量的goto语句,团队非常聪明,软件几乎没有缺陷。
所以,我们可以把goto的价值放在一边,如果问题仅仅是比较循环的话,我们可以通过分析和/或比较汇编代码来进行。然而,问题中包含了printf等语句。当进行这种操作时,你无法真正讨论循环控制逻辑的优化。此外,正如其他人指出的那样,给定的循环将会生成非常相似的机器代码。
在流水线处理器架构中,所有条件分支在解码阶段之前都被认为是“成功”的(true),此外,小循环通常会被扩展为无循环。因此,与Harper上面的观点一致,在简单的循环控制中,goto不太可能有任何优势(就像for和while彼此之间也没有优势一样)。在多重嵌套循环或多重嵌套if语句中,只要将goto的附加条件添加到每个嵌套循环或嵌套if语句中都是次优的。
在简单循环中优化搜索操作时,使用哨兵有时比其他任何方法都更有效。实际上,通过在数组末尾添加一个虚拟值,您可以避免检查两个条件(数组结尾和找到的值),只需检查一个条件(找到的值),这样可以节省内部的cmp操作。我不知道编译器是否会自动执行此操作。跳转到循环:
start_Chicken:
{
++x;
if (x >= loops)
goto end_Chicken;
}
goto start_Chicken;
end_Chicken:
x = 0;
for循环:
for (int i = 0; i < loops; i++)
{
}
while循环:
while (z <= loops)
{
++z;
}
z = 0;
在任何情况下,带有更多混合测试的while循环都具有最小但仍然更好的结果。
cmp
和jmp
。或者(根据给定的示例)在任何高于-O0
的级别上,它最终将直接调用printf() 3次。 - John Ledbettergoto
不是坏的“因为某个聪明人说它是”,他之所以这么说是因为它是有问题的。你对反对使用goto
的建议的轻率态度是没有根据的;有比goto
更好的表达意图的方式,这是毋庸置疑的。 - GManNickGprintf
的问题是可笑的。与循环控制相比,printf
会花费更多的时间。除非循环内有经过优化的代码(如无缓存失效等情况),否则处理循环控制本身的时间根本不值得考虑。 - Jens Gustedt