更深入地理解循环(for,while,do while,foreach,recursion等)

时间:2010-08-06 07:34:41

标签: loops performance memory-management

如果给你一些情况,你可以做一个需要使用循环解决的特定事件或函数的循环,你可以通过任何类型的循环来实现这些。我们如何根据速度,效率和内存使用情况确定可以使用的每个循环之间的区别?例如,你有这些循环

for(int i=0;i<10;i++) {
    array[i] = i+1;
}

int i = 0;
while(i<10) {
    array[i] = i+1;
    i++;
}

上面的例子有相同的输出,当然你在执行时看不出它们之间的区别,因为这只是一个小过程,如果你正处理一个耗费你记忆的巨大循环怎么办?哪个循环更好用?是否有适当的措施何时使用什么循环?

编辑:

对于pakore回答,

从你的答案中我可以有把握地说,如果我重新排序我的变量,其中大多数相关的变量彼此相距很远(中间的其他行)可能更有效。比如说,

a=0;
b=1;
c=5;
d=1;
for(int i=0; i<10;i++)
{
    a=b*CalculateAge()+d;
    m=c+GetAvarage(a);
    d++;
}

a=0;
b=1;
c=5;
d=1;
for(int i=0; i<10;i++)
{
    a=b*CalculateAge()+d;
    d++;
    m=c+GetAvarage(a);
}

后者比第一行效率更高,因为后者我在第一行调用了外部方法,第二行独立于第一行的结果而不是第三行的结果。

由于第一个示例将在执行第二行之前等待第一行的结果,而第三行的结果将在等待第一行的结果时执行第二行。

结论:

优化的循环不会影响您正在使用的循环类型。正如pakore所解释的那样,以及polygenelubricants,你在循环中可以想到的主要问题是你的代码是如何编写的。编译器的工作是优化代码,如果你根据每个变量对每个变量的依赖性来优化代码,它也会有所帮助,如下面的pakore所解释的那样。

8 个答案:

答案 0 :(得分:8)

嗯,这里很难解释循环背后的所有逻辑。

为了优化循环,编译器会为你做出惊人的事情,所以如果使用whilefor并不重要,因为编译器无论如何都会转换为汇编程序。

为了更深入地理解,你应该学习一些汇编程序,然后学习基本处理器的工作原理,它如何读取指令以及它如何处理它们。

为了改善流水线操作,最好将具有相同变量的语句放置在彼此远离的位置。这样,当计算一个语句时,处理器可以接受下一个语句,如果它独立于第一个语句并开始计算它。

例如:

a=0;
b=3;
c=5;
m=8;
i=0;
while(i<10){
a=a+b*c;
b=b*10+a;
m=m*5;
i++;
}

我们在ab之间存在依赖关系,并且这些语句彼此相邻。但我们发现mi与其他人无关,所以我们可以这样做:

a=0;
b=3;
c=5;
m=8;
i=0;
while(i<10){
a=a+b*c;
m=m*5;
i++;
b=b*10+a;

}

因此,在计算a时,我们可以开始计算mi。大多数情况下,编译器会检测到并自动执行此操作(它是调用代码重新排序)。有时候对于小循环,编译器会根据需要多次复制和粘贴循环中的代码,因为没有控制变量会更快。

我的建议是让编译器关注这些事情并关注你正在使用的算法的成本,最好从 O(n!)减少到 O( logn)而不是在循环内进行微优化。

根据修改后的问题进行更新

嗯,依赖关系必须是写/写或读/写依赖。如果它的读/读依赖性没有问题(因为值不会改变)。查看[数据依赖性文章](http://en.wikipedia.org/wiki/Data_dependency)。

<击> 在您的示例中,两个代码之间没有区别,m取决于cb,但这两个代码从不写入,因此编译器在进入循环之前知道它们的值。这是一个称为读/读依赖的,它本身不是一个依赖。

如果你写了:

...
m=c+GetAvarage(a);
...

然后我们会有一个写/读依赖(我们必须写a然后从a读取,所以我们必须等到{{1} }计算)你做的优化会很好。

但是再一次,编译器会为你和许多其他事情做这件事。很难说高级代码中的微优化会对汇编代码产生真正的影响,因为编译器可能已经为您做了这样的事情,或者可能正在为您重新排序代码,或者可能正在执行其他一千件事情比我们第一眼就能想到的要好。

但无论如何,知道如何在地毯下工作是好的:)

更新以添加一些链接 查看这些链接,以进一步了解编译器可以做些什么来提高代码性能:

Loop unwinding

Dependency Analysis

Automatic parallelization

Vectorization

答案 1 :(得分:3)

我将逐一解释每个循环案例,

1. For循环:当您确定 某些 没有迭代时,请转到 for 循环。

2. while循环:当您不确定没有迭代时,请转到while循环,或者您希望循环直到条件为false。

3. do-while :这与while循环相同,只是循环执行至少一次。

话虽如此,也可以为另一个案例编写一个循环。

4. 递归:如果您正确理解递归,递归会带来优雅的解决方案。 递归比直接迭代慢一点。

for,while,do-while之间没有性能差异。如果有的话,它们可以忽略不计。

答案 2 :(得分:2)

你应该写出最自然,惯用和可读的代码,清楚地传达你的意图。在大多数情况下,没有任何一个循环比另一个循环表现更好,以至于你牺牲了上述任何一个,但速度很快。

大多数主流语言的现代编译器在优化代码方面非常聪明,并且可以特别针对人们应该编写的各种可读代码。代码越复杂,人类就越难理解,编译器就越难以优化。

大多数编译器可以优化tail recursion,允许您递归地表达算法(在某些情况下这是最自然的形式),但实际上是迭代地执行它。否则,递归可能比迭代解决方案慢,但在进行此优化之前应考虑所有因素。

如果可以快速编写一个工作正确但可能稍微慢一点的递归解决方案,那么它通常更适合复杂的迭代解决方案,该解决方案可能更快但可能不是很明显正确和/或难以维护。

不要过早优化。

答案 3 :(得分:1)

任何体面的编译器都会生成相同的代码

为了测试这个,我创建了一个名为loops-f.c的文件:

void f(int array[])
{
    for(int i=0; i<10; i++) {
        array[i] = i+1;
    }
}

和名为loops-g.c的文件:

void g(int array[])
{
    int i = 0;
    while(i<10) {
        array[i] = i+1;
        i++;
    }
}

我将文件编译为程序集(gcc -std=c99 -S loops-f.c loops-g.c),然后我比较了生成的汇编代码(diff -u loops-f.s loops-g.s):

--- loops-f.s   2010-08-06 10:57:11.377196516 +0300
+++ loops-g.s   2010-08-06 10:57:11.389197986 +0300
@@ -1,8 +1,8 @@
-   .file   "loops-f.c"
+   .file   "loops-g.c"
    .text
-.globl f
-   .type   f, @function
-f:
+.globl g
+   .type   g, @function
+g:
 .LFB0:
    .cfi_startproc
    pushq   %rbp
@@ -30,6 +30,6 @@
    ret
    .cfi_endproc
 .LFE0:
-   .size   f, .-f
+   .size   g, .-g
    .ident  "GCC: (GNU) 4.4.4 20100630 (Red Hat 4.4.4-10)"
    .section    .note.GNU-stack,"",@progbits

正如您所看到的,代码几乎完全相同。

答案 4 :(得分:0)

循环的名称本身就可以了解用法。

当你只需要执行一个操作时,没有问题,for循环就可以了。

如果你正在迭代数据结构并且有一个约束,比如一个中断条件或类似的东西,应该选择while或do while循环。

答案 5 :(得分:0)

这只是个人偏好和编码风格的问题 - 首选风格也很大程度上取决于您编写的语言。

例如在python中,执行上述循环的首选方法看起来有点像:

for i in range(0, 9):
    array[i] = i + 1

(实际上在Python中,您可以在一行中完成上述操作:

array = range(1,10)

但它只是一个例子......)

在上述两个中,我倾向于使用第一个。

至于表现,你不太可能看到差异。

答案 6 :(得分:0)

它肯定取决于您使用的语言,但请记住,任何代码中最慢的部分是编码它的人,所以我建议为每种情况选择一个标准并坚持下去,然后当你来更新时代码你不需要每次都考虑这个。

如果您试图通过这种方式节省成本,那么您要么已经以接近100%的效率运行,要么可能在错误的位置加快代码?

答案 7 :(得分:0)

在Patterson和Hennessy的作者“计算机组织和设计:硬件/软件接口”一书中,将上面的循环转换为汇编,两个循环在MIPS中具有相同的汇编代码。

如果编译器在不同的汇编语句中编译两个循环,如果它们具有相同的性能,则会出现差异。