Difference between declaring variables before or in loop?
我一直在想,一般来说,在循环之前声明一个丢弃的变量,而不是在循环内部重复声明,是否会产生任何(性能)差异?Java中的一个(非常无意义的)例子:
(a)循环前声明:
ZZU1(b)循环内的语句(重复):
ZZU1哪个比较好,A还是B?
我怀疑重复变量声明(示例B)在理论上会产生更多的开销,但是编译器足够聪明,所以它并不重要。示例B的优点是更紧凑,并将变量的范围限制在使用位置。不过,我倾向于根据示例A编写代码。
编辑:我特别感兴趣的是Java案例。
哪个更好,A还是B?
从性能的角度来看,你必须衡量它。(在我看来,如果你能测量一个差异,编译器就不是很好了)。
从维护的角度来看,B更好。在尽可能窄的范围内,在同一位置声明和初始化变量。不要在声明和初始化之间留下空白,也不要污染您不需要的名称空间。
我把你的A和B示例分别运行了20次,循环了1亿次。(jvm-1.5.0)
A:平均执行时间:074秒
B:平均执行时间:.067秒
令我惊讶的是B的速度有点快。像计算机一样快,现在很难说你能不能精确地测量这个。我也会用A的方式来编码,但我会说这并不重要。
这取决于语言和确切的用法。例如,在C 1中,它没有任何区别。在C 2中,如果局部变量是由匿名方法(或C 3中的lambda表达式)捕获的,则会产生非常显著的差异。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | using System; using System.Collections.Generic; class Test { static void Main() { List<Action> actions = new List<Action>(); int outer; for (int i=0; i < 10; i++) { outer = i; int inner = i; actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer)); } foreach (Action action in actions) { action(); } } } |
输出:
1 2 3 4 5 6 7 8 9 10 | Inner=0, Outer=9 Inner=1, Outer=9 Inner=2, Outer=9 Inner=3, Outer=9 Inner=4, Outer=9 Inner=5, Outer=9 Inner=6, Outer=9 Inner=7, Outer=9 Inner=8, Outer=9 Inner=9, Outer=9 |
不同之处在于,所有操作都捕获相同的
以下是我在.NET中编写和编译的内容。
1 2 3 4 5 6 7 8 9 10 | double r0; for (int i = 0; i < 1000; i++) { r0 = i*i; Console.WriteLine(r0); } for (int j = 0; j < 1000; j++) { double r1 = j*j; Console.WriteLine(r1); } |
这是我从.NET Reflector得到的,当CIL被呈现回代码时。
1 2 3 4 5 6 7 8 9 10 | for (int i = 0; i < 0x3e8; i++) { double r0 = i * i; Console.WriteLine(r0); } for (int j = 0; j < 0x3e8; j++) { double r1 = j * j; Console.WriteLine(r1); } |
所以编译后两者看起来完全一样。在托管语言中,代码转换为cl/字节代码,在执行时转换为机器语言。因此,在机器语言中,甚至不能在堆栈上创建double。它可能只是一个寄存器,因为代码反映它是
这是vb.net中的一个gotcha。Visual Basic结果不会重新初始化此示例中的变量:
1 2 3 4 5 6 7 |
这将第一次打印0(声明时,Visual Basic变量具有默认值!)但在那之后,每次都是以东十一〔三〕号。
但是,如果您添加一个
1 2 3 4 5 6 7 |
我做了一个简单的测试:
1 2 3 4 | int b; for (int i = 0; i < 10; i++) { b = i; } |
VS
1 2 3 | for (int i = 0; i < 10; i++) { int b = i; } |
我用gcc-5.2.0编译了这些代码。然后我拆卸了主机这两个代码的结果是:
1O:
1 2 3 4 5 6 7 8 9 10 11 12 | 0x00000000004004b6 <+0>: push rbp 0x00000000004004b7 <+1>: mov rbp,rsp 0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0 0x00000000004004c1 <+11>: jmp 0x4004cd <main+23> 0x00000000004004c3 <+13>: mov eax,DWORD PTR [rbp-0x4] 0x00000000004004c6 <+16>: mov DWORD PTR [rbp-0x8],eax 0x00000000004004c9 <+19>: add DWORD PTR [rbp-0x4],0x1 0x00000000004004cd <+23>: cmp DWORD PTR [rbp-0x4],0x9 0x00000000004004d1 <+27>: jle 0x4004c3 <main+13> 0x00000000004004d3 <+29>: mov eax,0x0 0x00000000004004d8 <+34>: pop rbp 0x00000000004004d9 <+35>: ret |
VS
2O
1 2 3 4 5 6 7 8 9 10 11 12 | 0x00000000004004b6 <+0>: push rbp 0x00000000004004b7 <+1>: mov rbp,rsp 0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0 0x00000000004004c1 <+11>: jmp 0x4004cd <main+23> 0x00000000004004c3 <+13>: mov eax,DWORD PTR [rbp-0x4] 0x00000000004004c6 <+16>: mov DWORD PTR [rbp-0x8],eax 0x00000000004004c9 <+19>: add DWORD PTR [rbp-0x4],0x1 0x00000000004004cd <+23>: cmp DWORD PTR [rbp-0x4],0x9 0x00000000004004d1 <+27>: jle 0x4004c3 <main+13> 0x00000000004004d3 <+29>: mov eax,0x0 0x00000000004004d8 <+34>: pop rbp 0x00000000004004d9 <+35>: ret |
这与ASM结果完全相同。这两个代码产生的结果是一样的,这不是一个证据吗?
它依赖于语言——IIRC C对此进行了优化,因此没有任何区别,但JavaScript(例如)每次都会进行整个内存分配。
我总是使用(而不是依赖于编译器),也可能重写为:
1 2 3 4 | for(int i=0, double intermediateResult=0; i<1000; i++){ intermediateResult = i; System.out.println(intermediateResult); } |
这仍然将
在我看来,B是更好的结构。在A中,IntermediateResult的最后一个值在循环结束后保持不变。
编辑:这与值类型没有太大的区别,但是引用类型可能有点重。就我个人而言,我喜欢尽快取消引用变量以进行清理,而b会为您这样做,
我怀疑一些编译器可以将两者优化为相同的代码,但肯定不是全部。所以我想说你和前者相处得更好。后者的唯一原因是,如果您希望确保声明的变量只在循环中使用。
作为一般规则,我在尽可能内部的范围内声明变量。所以,如果您不在循环之外使用intermediateresult,那么我将使用b。
同事更喜欢第一个表单,告诉它是一个优化,更喜欢重用声明。
我更喜欢第二个(并且试着说服我的同事!;-)),读过:
- 它将变量的范围缩小到需要它们的地方,这是一件好事。
- Java优化足以在性能上没有显著差异。IIRC,也许第二种形式更快。
无论如何,它属于过早优化的范畴,依赖于编译器和/或JVM的质量。
好吧,你总是可以为这个做一个范围:
1 2 3 4 5 6 7 | { //Or if(true) if the language doesn't support making scopes like this double intermediateResult; for (int i=0; i<1000; i++) { intermediateResult = i; System.out.println(intermediateResult); } } |
这样,您只声明一次变量,当您离开循环时,它将死亡。
如果您在lambda等中使用变量,则C中存在差异。但通常情况下,编译器将执行相同的操作,假设变量仅在循环中使用。
考虑到它们基本上是相同的:请注意,版本B使读者更明显地认识到变量在循环之后不是也不能使用。此外,版本B更容易重构。在版本A中,将循环体提取到它自己的方法中更为困难。此外,版本B向您保证,这样的重构没有副作用。
因此,版本A让我非常恼火,因为它没有任何好处,而且它使得对代码的解释变得更加困难…
我一直认为,如果在循环中声明变量,那么就是在浪费内存。如果你有这样的东西:
然后,不仅需要为每个迭代创建对象,还需要为每个对象分配一个新的引用。如果垃圾收集器运行缓慢,那么您将有大量悬空的引用需要清理。
但是,如果您有:
然后,您只需要创建一个引用,并每次为其分配一个新对象。当然,超出范围可能需要更长的时间,但是只有一个悬而未决的引用需要处理。
我认为这取决于编译器,很难给出一般的答案。
我的做法是:
如果变量的类型很简单(int,double,…),我更喜欢变量b(内部)。原因:变量范围缩小。
如果变量类型不简单(某种类型的
class 或struct ,我更喜欢变量A(外部)。原因:减少ctor dtor调用的数量。
从性能的角度来看,外部更好。
1 2 3 4 5 6 7 8 9 10 11 12 |
我执行了这两个函数10亿次。Outside()花费了65毫秒。内侧()用了1.5秒。
我有这个问题很久了。所以我测试了一段更简单的代码。
结论:在这种情况下,没有表现差异。
外回路箱
1 2 3 4 5 | int intermediateResult; for(int i=0; i < 1000; i++){ intermediateResult = i+2; System.out.println(intermediateResult); } |
内环箱
1 2 3 4 | for(int i=0; i < 1000; i++){ int intermediateResult = i+2; System.out.println(intermediateResult); } |
我检查了intellij的反编译器上编译的文件,对于这两种情况,我得到了相同的
1 2 3 4 | for(int i = 0; i < 1000; ++i) { int intermediateResult = i + 2; System.out.println(intermediateResult); } |
我还使用这个答案中给出的方法分解了这两个案例的代码。我只显示与答案相关的部分
外回路箱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_2 2: iload_2 3: sipush 1000 6: if_icmpge 26 9: iload_2 10: iconst_2 11: iadd 12: istore_1 13: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 16: iload_1 17: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 20: iinc 2, 1 23: goto 2 26: return LocalVariableTable: Start Length Slot Name Signature 13 13 1 intermediateResult I 2 24 2 i I 0 27 0 args [Ljava/lang/String; |
内环箱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: sipush 1000 6: if_icmpge 26 9: iload_1 10: iconst_2 11: iadd 12: istore_2 13: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 16: iload_2 17: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 20: iinc 1, 1 23: goto 2 26: return LocalVariableTable: Start Length Slot Name Signature 13 7 2 intermediateResult I 2 24 1 i I 0 27 0 args [Ljava/lang/String; |
如果您密切注意,只有分配给
- 没有执行额外的操作
- 在这两种情况下,
intermediateResult 仍然是一个局部变量,因此没有不同的访问时间。
奖金
编译器做了大量的优化,看看在这种情况下会发生什么。
零工作案例
1 2 3 4 |
零功反编译
1 2 3 |
a)是安全的赌注而不是b)……想象一下,如果您是在循环中初始化结构,而不是"int"或"float",那么是什么?
喜欢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | typedef struct loop_example{ JXTZ hi; // where JXTZ could be another type...say closed source lib // you include in Makefile }loop_example_struct; //then.... int j = 0; // declare here or face c99 error if in loop - depends on compiler setting for ( ;j++; ) { loop_example loop_object; // guess the result in memory heap? } |
你肯定会面临内存泄漏的问题!因此,我认为"a"是更安全的赌注,而"b"则容易受到内存累积的影响,特别是在接近源代码库的情况下。您可以在Linux上使用"valgrind"工具,特别是子工具"helgrind"进行检查。
这是一个有趣的问题。根据我的经验,当你为代码争论这个问题时,有一个终极问题需要考虑:
变量需要是全局变量有什么原因吗?
只声明一次全局变量(而不是多次局部声明)是有意义的,因为这样更利于组织代码,并且需要的代码行更少。但是,如果只需要在一个方法中本地声明它,那么我将在该方法中对其进行初始化,这样很明显,变量只与该方法相关。如果选择后一个选项,请注意不要在初始化该变量的方法之外调用它——您的代码将不知道您在说什么,并且会报告一个错误。
另外,作为补充说明,即使不同方法的目的几乎相同,也不要在它们之间复制局部变量名;这会让人困惑。
如果有人感兴趣,我用node 4.0.0测试了JS。在循环外声明导致了大约5毫秒的性能改进,平均超过1000个测试,每个测试有1亿个循环迭代。所以我要说"继续",用最可读/可维护的方式来写它,即b,imo。我会把我的代码放在一把小提琴上,但我使用了现在的性能节点模块。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | var now = require("../node_modules/performance-now") // declare vars inside loop function varInside(){ for(var i = 0; i < 100000000; i++){ var temp = i; var temp2 = i + 1; var temp3 = i + 2; } } // declare vars outside loop function varOutside(){ var temp; var temp2; var temp3; for(var i = 0; i < 100000000; i++){ temp = i temp2 = i + 1 temp3 = i + 2 } } // for computing average execution times var insideAvg = 0; var outsideAvg = 0; // run varInside a million times and average execution times for(var i = 0; i < 1000; i++){ var start = now() varInside() var end = now() insideAvg = (insideAvg + (end-start)) / 2 } // run varOutside a million times and average execution times for(var i = 0; i < 1000; i++){ var start = now() varOutside() var end = now() outsideAvg = (outsideAvg + (end-start)) / 2 } console.log('declared inside loop', insideAvg) console.log('declared outside loop', outsideAvg) |
这是比较好的形式
1 2 3 4 5 6 7 8 | double intermediateResult; int i = byte.MinValue; for(; i < 1000; i++) { intermediateResult = i; System.out.println(intermediateResult); } |
1)以这种方式声明一次时间都是变量,而不是每个都是循环。2)任务是所有其他选项中最重要的。3)所以最佳实践规则是迭代之外的任何声明。
在go中尝试了同样的方法,并将使用
零差,根据汇编程序输出。
即使我知道我的编译器足够聪明,我也不想依赖它,并且会使用a)变量。
只有当你迫切需要在循环体之后使中间结果不可用时,b)变量才对我有意义。但无论如何,我无法想象如此绝望的局面……
编辑:JonSkeet提出了一个很好的观点,表明循环中的变量声明可以产生实际的语义差异。