[英]Deeper understanding on loops (for, while, do while, foreach, recursion, etc.)
如果给你一些情况,你可以做一个需要使用循环解决的特定事件或函数的循环,你可以通过任何类型的循环来实现这些。 我们如何根据速度,效率和内存使用情况确定可以使用的每个循环之间的区别? 例如,你有这些循环
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所解释的那样。
好吧,这里很难解释循环背后的所有逻辑。
为了优化循环,编译器会为你做一些惊人的事情,所以如果你使用while
或for
无关紧要for
因为编译器无论如何都会转换为汇编程序。
为了更深入地理解,您应该学习一些汇编程序,然后学习基本处理器的工作原理,它如何读取指令以及如何处理它们。
为了改进流水线操作,最好将具有相同变量的语句放在彼此远离的位置。 这样,当计算一个语句时,处理器可以采用下一个语句,如果它独立于第一个语句并开始计算它。
例如:
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++;
}
我们在a
和b
之间有一个依赖关系,而且这些语句是紧挨着的。 但我们看到m
和i
独立于其他人,所以我们可以这样做:
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
被计算,我们就可以开始计算m
和i
。 大多数情况下,编译器会检测到并自动执行此操作(它是调用代码重新排序)。 有时候对于小循环,编译器会根据需要多次复制和粘贴循环中的代码,因为没有控制变量会更快。
我的建议是让编译器关注这些事情并关注你正在使用的算法的成本,最好从O(n!)减少到O(logn),而不是在循环内进行微优化。
根据修改的问题更新
好吧,依赖关系必须是写/写或读/写依赖。 如果它的读/读依赖性没有问题(因为值不会改变)。 查看[数据依赖性文章]( http://en.wikipedia.org/wiki/Data_dependency )。
在您的示例中,两个代码之间没有区别,
m
取决于
c
和
b
但这两个从不写,因此编译器在进入循环之前知道它们的值。
这称为读/读依赖,它本身不是依赖。
如果你写了:
... m=c+GetAvarage(a); ...
然后我们将有一个写/读依赖(我们必须写入
a
然后从
a
读取,所以我们必须等到
a
计算)并且你做的优化会很好。
但是再次,编译器会为您和许多其他事情做这件事。 很难说高级代码中的微优化会对汇编代码产生真正的影响,因为编译器可能已经为您做了这样的事情,或者可能正在为您重新排序代码,或者可能正在执行其他一千件事情比我们乍一看还要好。
但无论如何,知道如何在地毯下工作是件好事:)
更新以添加一些链接查看这些链接以进一步了解编译器可以做些什么来提高代码性能:
我将逐一解释每个循环案例,
1. For循环:当你确信你做迭代一定没有,然后去进行循环。
2. while循环:当您不确定没有迭代时,请转到while循环,或者您希望循环直到条件为false。
3. do-while :这与while循环相同,只是循环执行至少一次。
话虽如此,也可以为另一个案例编写一个循环。
4. 递归 :如果你正确理解递归,递归会带来优雅的解决方案。 递归比直接迭代慢一点。
for,while,do-while之间没有性能差异。如果有的话,它们可以忽略不计。
你应该写出最自然,惯用和可读的代码,清楚地传达你的意图。 在大多数情况下,没有一个循环比另一个循环表现更好,以至于你牺牲了上述任何一个以获得很少的速度。
大多数主流语言的现代编译器在优化代码方面非常聪明,并且可以特别针对人们应该编写的各种可读代码。 代码越复杂,人类就越难理解,编译器就越难以优化。
大多数编译器可以优化尾递归 ,允许您递归地表达算法(在某些情况下这是最自然的形式),但实质上是迭代地执行它。 否则,递归可能比迭代解决方案慢,但在进行此优化之前应考虑所有因素。
如果一个工作的,正确的,但也许一点点慢的递归解决方案可以快速编写,那么它通常更喜欢复杂的迭代解决方案,可能更快但可能不明显正确和/或更难维护。
不要过早优化。
任何体面的编译器都会生成相同的代码 。
为了测试这个,我创建了一个名为loops-fc
的文件:
void f(int array[])
{
for(int i=0; i<10; i++) {
array[i] = i+1;
}
}
和一个名为loops-gc
的文件:
void g(int array[])
{
int i = 0;
while(i<10) {
array[i] = i+1;
i++;
}
}
我将文件编译为程序集( gcc -std=c99 -S loops-fc loops-gc
)然后我比较了生成的汇编代码( diff -u loops-fs loops-gs
):
--- 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
正如您所看到的,代码几乎完全相同。
循环的名称本身给出了关于用法的想法。
当你只需要执行一个操作,没有问题时,for循环就可以了。
如果您正在迭代数据结构并具有约束,如中断条件或类似的东西,则应选择while或while while循环。
它只是个人偏好和编码风格的问题 - 首选风格也很大程度上取决于您编写的语言。
例如在python中,执行上述循环的首选方法看起来有点像:
for i in range(0, 9):
array[i] = i + 1
(实际上在Python中,您可以在一行中完成上述操作:
array = range(1,10)
但它只是一个例子...)
在上述两个中,我倾向于第一个。
至于性能,你不太可能看到差异。
它肯定取决于你使用的语言,但请记住,任何代码中最慢的部分是编码它的人,所以我建议为每种情况选择一个标准并坚持下去,然后当你来更新代码时每次都需要考虑这个问题。
如果您试图通过这种方式节省成本,那么您要么已经以接近100%的效率运行,要么可能在错误的位置加快代码?
在Patterson和Hennessy作者的“计算机组织和设计:硬件/软件接口”一书中,将上面的循环转换为汇编,两个循环在MIPS中具有相同的汇编代码。
如果编译器在不同的汇编语句中编译两个循环(如果它们具有相同的性能),则会出现差异。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.