我一直想知道,一般来说,在循环之前声明一个抛弃变量,而不是反复在循环内,是否会产生任何(性能)差异? Java中的(毫无意义)示例:
循环前的a)声明:
double intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
内部循环b)声明(重复):
for(int i=0; i < 1000; i++){
double intermediateResult = i;
System.out.println(intermediateResult);
}
哪一个更好,一个或 b ?
我怀疑重复的变量声明(例如 b )会在理论上产生更多的开销 ,但编译器足够智能,因此无关紧要。示例 b 具有更紧凑的优点,并将变量的范围限制为使用它的位置。不过,我倾向于根据示例编写 a 。
编辑: 我对Java案件特别感兴趣。
答案 0 :(得分:245)
哪个更好, a 或 b ?
从性能角度来看,您必须对其进行衡量。 (在我看来,如果你可以测量差异,编译器就不是很好了。)
从维护角度来看, b 更好。在尽可能最窄的范围内在同一位置声明和初始化变量。不要在声明和初始化之间留下空洞,也不要污染您不需要的命名空间。
答案 1 :(得分:208)
我运行你的A和B示例各20次,循环1亿次。(JVM - 1.5.0)
A:平均执行时间:.074秒
B:平均执行时间:.067秒
令我惊讶的是B稍快一些。 如果能够准确地测量计算机,现在很难说计算机。 我会把它编码为A方式,但我会说它并不重要。
答案 2 :(得分:66)
这取决于语言和确切用途。例如,在C#1中没有任何区别。在C#2中,如果局部变量是通过匿名方法(或C#3中的lambda表达式)捕获的,那么它可以产生非常显着的差异。
示例:
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();
}
}
}
输出:
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
不同之处在于,所有操作都捕获相同的outer
变量,但每个变量都有自己独立的inner
变量。
答案 3 :(得分:35)
以下是我在.NET中编写和编译的内容。
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呈现回代码时的结果。
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。它可能只是一个寄存器,因为代码反映它是WriteLine
函数的临时变量。循环有一整套优化规则。所以一般人不应该担心它,特别是在托管语言中。在某些情况下,您可以优化管理代码,例如,如果您必须仅使用string a; a+=anotherstring[i]
与使用StringBuilder
来连接大量字符串。两者之间的表现有很大差异。有很多这样的情况,编译器无法优化您的代码,因为它无法弄清楚更大范围内的目标。但它可以为你优化基本的东西。
答案 4 :(得分:24)
这是VB.NET中的问题。在此示例中,Visual Basic结果不会重新初始化变量:
For i as Integer = 1 to 100
Dim j as Integer
Console.WriteLine(j)
j = i
Next
' Output: 0 1 2 3 4...
这将在第一次打印0时(Visual Basic变量在声明时具有默认值!),但在此之后每次都会i
。
但是,如果您添加= 0
,则会得到您所期望的结果:
For i as Integer = 1 to 100
Dim j as Integer = 0
Console.WriteLine(j)
j = i
Next
'Output: 0 0 0 0 0...
答案 5 :(得分:15)
我做了一个简单的测试:
int b;
for (int i = 0; i < 10; i++) {
b = i;
}
vs
for (int i = 0; i < 10; i++) {
int b = i;
}
我用gcc-5.2.0编译了这些代码。然后我拆卸了主() 这两个代码中的结果就是结果:
1º:
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
2º
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
其中exaclty与结果相同。是不是两个代码产生相同的东西的证明?
答案 6 :(得分:11)
它依赖于语言 - IIRC C#对此进行优化,因此没有任何区别,但JavaScript(例如)每次都会执行整个内存分配。
答案 7 :(得分:11)
我总是使用A(而不是依赖于编译器),也可能会重写为:
for(int i=0, double intermediateResult=0; i<1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
这仍然会将intermediateResult
限制在循环的范围内,但在每次迭代期间不会重新声明。
答案 8 :(得分:6)
在我看来,b是更好的结构。在a中,在循环结束后,intermediateResult的最后一个值会保持不变。
编辑: 这与值类型没有太大区别,但引用类型可能有些重要。就个人而言,我喜欢尽快解除引用的变量以进行清理,b为你做这件事,
答案 9 :(得分:5)
我怀疑一些编译器可以将两者优化为相同的代码,但肯定不是全部。所以我会说你跟前者比较好。后者的唯一原因是,如果您想确保声明的变量在循环中仅使用 。
答案 10 :(得分:5)
作为一般规则,我在最可能的范围内声明我的变量。所以,如果你没有在循环之外使用intermediateResult,那么我会选择B。
答案 11 :(得分:5)
同事更喜欢第一种形式,告诉它是一种优化,更喜欢重复使用声明。
我更喜欢第二个(并试图说服我的同事!;-)),读过:
无论如何,它属于依赖于编译器和/或JVM质量的过早优化类别。
答案 12 :(得分:5)
如果你在lambda等中使用变量,那么C#就有区别。但是通常编译器基本上会做同样的事情,假设变量只在循环中使用。
鉴于它们基本相同:请注意,版本b使得读者更加明显地认为变量不是,也不能在循环之后使用。此外,版本b更容易重构。在版本a中将循环体提取到自己的方法中更加困难。此外,版本b确保对这种重构没有副作用。
因此,版本a让我感到厌烦,因为它没有任何好处,它使代码更难以推理......
答案 13 :(得分:5)
嗯,你总是可以为此做一个范围:
{ //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);
}
}
这样你只需要声明一次变量,当你离开循环时它就会死掉。
答案 14 :(得分:4)
我一直认为,如果你在循环中声明变量,那么你就是在浪费内存。如果您有这样的事情:
for(;;) {
Object o = new Object();
}
然后,不仅需要为每次迭代创建对象,而且需要为每个对象分配新的引用。似乎如果垃圾收集器很慢,那么你将需要清理一堆悬空引用。
但是,如果你有这个:
Object o;
for(;;) {
o = new Object();
}
然后,您每次只创建一个引用并为其分配一个新对象。当然,它可能需要更长的时间才能超出范围,但是只有一个悬空参考可以处理。
答案 15 :(得分:3)
我认为这取决于编译器,很难给出一般答案。
答案 16 :(得分:3)
我的做法是:
如果变量类型很简单(int,double,...)我更喜欢变体 b (内部)。
原因:减少变量的范围。
如果变量类型不简单(某种class
或struct
)我更喜欢变体 a (外部) 。
原因:减少了ctor-dtor电话的数量。
答案 17 :(得分:1)
从表现的角度来看,外面的(好)更好。
public static void outside() {
double intermediateResult;
for(int i=0; i < Integer.MAX_VALUE; i++){
intermediateResult = i;
}
}
public static void inside() {
for(int i=0; i < Integer.MAX_VALUE; i++){
double intermediateResult = i;
}
}
我分别执行了10亿次这两个功能。 outside()花了65毫秒。 inside()用了1.5秒。
答案 18 :(得分:0)
A)比B)安全下注.......想象一下,如果你在循环中初始化结构而不是'int'或'float'那么什么?
喜欢
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'。
答案 19 :(得分:0)
这是一个有趣的问题。根据我的经验,当您为代码辩论此问题时,需要考虑一个终极问题:
这个变量需要是全局的吗?
仅在全局范围内声明变量一次是有意义的,而不是在本地多次声明变量,因为它更好地组织代码并且需要更少的代码行。但是,如果它只需要在一个方法中在本地声明,我会在该方法中初始化它,因此很明显该变量与该方法完全相关。如果选择后一个选项,请注意不要在初始化方法之外调用此变量 - 您的代码将不知道您正在谈论的内容并报告错误。
另外,作为附注,不要在不同方法之间复制局部变量名,即使它们的目的几乎相同;它只是让人困惑。
答案 20 :(得分:0)
如果有兴趣的话,我用Node 4.0.0测试了JS。在循环外声明导致平均超过1000次试验的性能提高约0.5ms,每次试验有1亿次循环迭代。所以我要说继续以最可读/可维护的方式编写它,即B,imo。我会把我的代码放在一个小提琴中,但我使用了性能现在的Node模块。这是代码:
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)
答案 21 :(得分:0)
这是更好的形式
double intermediateResult;
int i = byte.MinValue;
for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}
1)以这种方式声明一次两个变量,而不是每个循环。 2)分配它的所有其他选项。 3)所以bestpractice规则是迭代之外的任何声明。
答案 22 :(得分:0)
在Go中尝试了同样的事情,并使用go tool compile -S
将编译器输出与go 1.9.4进行了比较
零差异,根据汇编程序输出。
答案 23 :(得分:0)
很长一段时间我都遇到过同样的问题。所以我测试了一个更简单的代码。
结论:对于此类情况,没有性能差异。
外循环案例
int intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i+2;
System.out.println(intermediateResult);
}
内部循环案例
for(int i=0; i < 1000; i++){
int intermediateResult = i+2;
System.out.println(intermediateResult);
}
我在IntelliJ的反编译器上检查了已编译的文件,在两种情况下,我都得到了相同 Test.class
for(int i = 0; i < 1000; ++i) {
int intermediateResult = i + 2;
System.out.println(intermediateResult);
}
我还使用此answer中给出的方法针对这两种情况反汇编了代码。我只会显示与答案有关的部分
外循环案例
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;
内部循环案例
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;
如果您密切注意,则仅将Slot
中分配给i
和intermediateResult
的{{1}}交换为它们出现顺序的乘积。相同的插槽差异反映在其他代码行中。
LocalVariableTable
在两种情况下仍是局部变量,因此访问时间没有差异。奖励
编译器做了很多优化,看看在这种情况下会发生什么。
零工作案例
intermediateResult
零工作已反编译
for(int i=0; i < 1000; i++){
int intermediateResult = i;
System.out.println(intermediateResult);
}
答案 24 :(得分:0)
当我想在退出循环后查看变量的内容时,我使用 (A)。它只对调试很重要。当我希望代码更紧凑时,我使用 (B),因为它可以节省一行代码。
答案 25 :(得分:-1)
即使我知道我的编译器足够聪明,我也不会依赖它,并且会使用a)变体。
只有当你迫切需要在循环体之后使 intermediateResult 不可用时,b)变体对我才有意义。但无论如何,我无法想象这种绝望的情况......编辑: Jon Skeet 提出了一个非常好的观点,表明循环中的变量声明可以产生实际的语义差异。