for()和while()之间有性能差异吗?

21

还是说这一切只是关于语义学?


9
因为可能存在一些不太明显的情况,这些情况下某种方法确实会产生实际影响,而仅进行一些小的基准测试并不能检测到这些情况。 - RichieHindle
3
与其获取一些客观数据,还不如进行一次对几个 Stack Overflow 用户的民意调查,而这些用户都在使用“可能”和“也许”等模棱两可的措辞?如果我真的关心答案,就不会采用这种方法。 - anon
7
@Neil: 不,我会给你五个理由 - 1)当涉及基准测试时,我不太熟练。我可能会做500万次迭代,但我不会相信结果。2)即使简单的基准测试显示相同的速度,我也不知道是否存在一些特殊情况下结果可能会有所不同。3)我需要比编译器更快地得到答案。4)为了改善SO数据库中的问题/答案范围。5)为了获得一千个新声望点数...说实话,我看到你到处都在投反对票和关闭投票,请别再这样做了。 - sharkin
3
这里的回答数量让我难以相信,它们在回答一个明确有答案的问题时只给出了“观点”。另外,我建议从问题标题中删除“值得注意的”,因为每个程序员对这个词的定义可能会有很大的不同。这个问题要么有差别,要么没有差别。 - Binary Worrier
5
@Neil:即使R.A能够编写一个循环来执行500万次迭代,他很可能不会碰到那些真正有区别的有趣情况,因为他不知道应该去寻找它们。在SO上众包答案可以提供如有必要进行测试的想法。 Translated: 即使 R.A 能够编写一个循环来做 500 万次迭代,他也很可能不会触及那些真正有意义的情况,因为他不知道应该去寻找这些情况。在 Stack Overflow 上向大众求解可以提供一些测试的想法,如果有必要的话。 - Nathan Fellman
显示剩余19条评论
16个回答

65

简短回答:不,它们完全相同。

理论上可能取决于编译器;一个非常糟糕的编译器可能会做些微不同的事情,但我会感到惊讶。

只是为了好玩,这里有两个变量,使用x86 gcc版本4.3.3作为Ubuntu附带的编译器,对我而言,它们编译成完全相同的汇编代码。您可以在linux上使用objdump检查最终二进制文件生成的汇编代码。

int main()
{
#if 1
    int i = 10;
    do { printf("%d\n", i); } while(--i);
#else
    int i = 10;
    for (; i; --i) printf("%d\n", i);
#endif
}

编辑: 这里有一个“苹果和苹果”while循环示例,它也编译成相同的汇编代码:

    while(i) { printf("%d\n", i); --i; }

21
支持进行实验,而不仅仅是“表达意见”。 - Binary Worrier
6
通过进行实验来进行尽职调查,这是一个更值得肯定的做法,非常好的回答!+1。 - Adam McKee
4
-1 用苹果(for)和橙子(do-while)进行比较通常是不等价的。您应该使用功能等效的 while 循环代替。 - starblue
1
+1 是说这取决于编译器。使用我的 eco32 gcc 后端(RISC,非常简单的体系结构)它也编译成完全相同的代码(gcc 4.4)。 - Johannes Schaub - litb
1
应该使用“while”,而不是“do-while”。只有在第一个printf之前的#else情况下才测试“i”。 - Drew Dormann
显示剩余5条评论

14
如果你的for循环和while循环执行相同的任务,那么编译器生成的机器代码应该(几乎)相同。
例如,在我几年前进行的一些测试中,
for (int i = 0; i < 10; i++)
{
...
}
并且
int i = 0;
do
{
  ...
  i++;
}
while (i < 10);

会生成完全相同的代码,或者(正如Neil在评论中指出的那样)多一个 jmp,但这不会对性能产生足够大的影响而值得担心。


1
你有什么确凿的证据来支持你的说法吗?“should be” - Binary Worrier
11
实际上,在我的VC++ 6.0编译器上,它们并没有生成完全相同的代码。使用for()版本会生成额外的jmp指令。 - anon
@Neil:把你的实验发布为可复现的答案,怎么样? - sharkin
2
在人们开始兴奋之前,我应该指出jmp只被执行一次,无论循环迭代多少次。我的观点是代码并不相同,人们应该通过实证检查这些事情。 - anon
你为何把while改为do-while呢?在某些情况下,它可能会稍微快一点,但在其他情况下,它会让你付出代价。 - aib
我将while改为do-while,因为回想起几年前做的测试,for循环会跳转到结尾的测试。我希望我在家里,这样我就可以直接生成代码并进行比较,而不是试图记住它。 - Paul Tomblin

8

没有语义差异,也不需要编译差异。但这取决于编译器。所以我尝试了g++ 4.3.2、CC 5.5和xlc6。

g++、CC是相同的,而xlc则不同

xlc中的差别在于初始循环进入点。

extern int doit( int );
void loop1( ) {
  for ( int ii = 0; ii < 10; ii++ ) {
    doit( ii );
  }
}

void loop2() {
  int ii = 0; 
  while ( ii < 10 ) {
    doit( ii );
    ii++;
  }
}

XLC OUTPUT

.loop2:                                 # 0x00000000 (H.10.NO_SYMBOL)
    mfspr   r0,LR
    stu     SP,-80(SP)
    st      r0,88(SP)
    cal     r3,0(r0)
    st      r3,64(SP)
    l       r3,64(SP)  ### DIFFERENCE ###
    cmpi    0,r3,10
    bc      BO_IF_NOT,CR0_LT,__L40
...
enter code here
.loop1:                                 # 0x0000006c (H.10.NO_SYMBOL+0x6c)
    mfspr   r0,LR
    stu     SP,-80(SP)
    st      r0,88(SP)
    cal     r3,0(r0)
    cmpi    0,r3,10    ### DIFFERENCE ###
    st      r3,64(SP)
    bc      BO_IF_NOT,CR0_LT,__La8
...

7
while循环中变量的作用域比for循环头声明的变量的作用域更广。因此,如果保持一个变量的生存时间会产生性能影响,则在选择使用while循环还是for循环时(不要将while循环包裹在{}中以减少其变量的作用域),将会产生性能影响。
例如,有一个并发集合,该集合计算引用它的迭代器数量,并且如果存在多个迭代器,则应用锁来防止并发修改,但作为一种优化,如果只有一个迭代器引用它,则可以省略锁定。如果在同一个容器上使用了两个具有不同名称的迭代器的函数中有两个for循环,那么将采用快速路径,但是使用两个while循环将采用慢速路径。类似地,如果对象很大(造成更多缓存流量)或使用系统资源,则可能会产生性能影响。但我无法想到一个真正的例子,说明这将有所不同。

我喜欢“将while循环放在{}中以减少其变量的作用域”。 - Thomas L Holaday
但这假设的意图是让变量有更长的生命周期。for()的好处在于,您可以选择声明一个变量,其作用域为循环的生命周期。而while()不提供这个选项。 - Julie in Austin

6

使用循环展开进行优化的编译器可能只在for循环情况下执行此操作。


1
非常好的观点!很有洞察力,我认为很多人都会忽略。优化是一个不能被忽视的方面。 - Kekoa

5

两者是等价的,这只是语义上的区别。

唯一的区别可能在于do... while结构中,您将条件的评估推迟到体之后,因此可能节省1次评估。

i = 1; do { ... i--; } while( i > 0 ); 

与之相反
for(  i = 1; i > 0; --i )
{ ....
}  

4

我是一名编译器开发者。我们将所有的“结构化”控制流(如ifwhileforswitchdo...while)都编译成条件和无条件分支语句,然后分析控制流图。由于C编译器必须处理一般的goto语句,所以最简单的方法是将所有内容都简化为分支和条件分支指令,然后确保处理这种情况。 (C编译器不仅要处理手写代码,还要处理自动生成的代码,其中可能有许多goto语句。)


我也编写过编译器和其他代码生成器。低级优化是编写编译器的人们感兴趣的一个重要主题。对于使用编译器的人来说,通过避免过度设计,有更大的方法来影响代码的速度。 - Mike Dunlavey
一些较新的工具可以在语句级别提供统计信息,但仍然困扰于计算执行次数和/或时间持续时间的概念。您需要将它们乘以某个感兴趣的时间间隔内的总时间,然后除以一些总时间,以开始接近问题的核心。 - Mike Dunlavey
他们还固执地认为,1)统计数据必须具有高精度,2)天哪,我们不能让程序变慢。 - Mike Dunlavey
@Mike:我仍在等待一个真正好用的分析器。像gprof这样的PC采样工具还可以,但是处理递归和虚方法调用方面存在问题。成本中心分析和valgrind都有可取之处,但总体而言,我希望看到更好的工具和更高的最低标准。 - Norman Ramsey
写编译器的确是一件不错的事情,尽管我上一次写已经很久远了,而且编译到解释语言。当试图理解性能以及为什么这些问题有点无意义时,了解“高级”控制结构实际上是如何工作的,以及在执行期间如何将变量放入堆栈中会有所帮助。 - Julie in Austin
显示剩余3条评论

3

理想情况下应该是相同的,但最终取决于您的编译器/解释器。为了确保,您必须测量或检查生成的汇编代码。

有可能存在差异的证明:这些行使用cc65生成不同的汇编代码。

for (; i < 1000; ++i);   
while (i < 1000) ++i;

3

不会。如果它们执行的是相同的操作,它们将编译成相同的代码 - 就像你所说的,这关乎语义。选择最能表达你想要表达的内容的那个。


1

continueforwhile中的行为不同:在for中,它改变计数器的值,在while中,通常不会改变计数器的值


你在说哪个计数器? - sharkin
在下面的代码中,i是一个计数器:for ( i = 1; i < 10; ++i) { ... }i = 1; while ( i < 10) { ... ; ++i } - dmityugov
1
while()循环中没有计数器,只有循环控制表达式。 - Julie in Austin

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