在循环前或循环中声明变量的区别?

我一直想知道,在一般情况下,在循环之前声明一个丢弃变量,而不是在循环内部重复,使任何(性能)差异? Java中的(很)示例:

在循环之前一)声明:

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的优点是更紧凑,并将变量的作用域限制在它所使用的地方。尽管如此,我还是倾向于根据例子一个来编码。

我对Java的情况特别感兴趣。

145924 次浏览

我认为这取决于编译器,很难给出一个一般的答案。

这取决于语言- IIRC c#优化了这一点,所以没有任何区别,但JavaScript(例如)每次都会完成整个内存分配过程。

即使我知道我的编译器足够聪明,我也不喜欢依赖它,而是使用a)变体。

b)变体对我来说只有当你迫切需要在循环体之后使intermediateResult不可用时才有意义。但我无法想象这种绝望的情况,无论如何....

EDIT: Jon双向飞碟提出了一个很好的观点,它表明在循环中声明变量可以产生实际的语义差异。

我怀疑一些编译器可以将两者优化为相同的代码,但肯定不是全部。所以我觉得你还是选择前者比较好。使用后者的唯一原因是如果你想确保在循环中使用声明的变量只有

作为一般规则,我将变量声明在最内部的可能范围内。如果你不在循环外使用intermediateResult,那么我会用B。

这取决于语言和确切的用法。例如,在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变量。

我认为b是更好的结构。在a中,intermediateResult的最后一个值在循环结束后保留。

< p >编辑: 这与值类型没有太大区别,但引用类型可能会有些重要。就我个人而言,我喜欢尽快解除对变量的引用以进行清理,b为您做了这一点,

一个b哪个更好?

从性能的角度来看,你必须衡量它。(在我看来,如果你能测量到差异,编译器不是很好)。

从维护的角度来看,b更好。在同一个地方声明和初始化变量,在尽可能窄的范围内。不要在声明和初始化之间留下空隙,不要污染不需要的名称空间。

我总是使用A(而不是依赖于编译器),也可能重写为:

for(int i=0, double intermediateResult=0; i<1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}

这仍然将intermediateResult限制在循环的范围内,但不会在每次迭代期间重新声明。

这是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...

我把A和B的例子各运行了20次,循环了1亿次。(jvm - 1.5.0)

A:平均执行时间:0.074秒

B:平均执行时间:0.067秒

令我惊讶的是B稍微快一点。 尽管现在的计算机速度很快,但很难说你是否能准确地测量这一点。 我也会用A的方式来编码,但我想说这并不重要

一位同事更喜欢第一种形式,说这是一种优化,更喜欢重用声明。

我更喜欢第二种(试着说服我的同事!);-)),已读到:

  • 它将变量的范围缩小到需要的地方,这是一件好事。
  • Java进行了足够的优化,在性能上没有显著差异。IIRC,也许第二种形式更快。

无论如何,它属于依赖于编译器和/或JVM质量的过早优化的类别。

以下是我在。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);
}

这是当CIL被呈现回代码时,我从. net Reflector得到的结果。

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连接大量字符串。两者在性能上有很大的不同。有很多这样的情况,编译器不能优化你的代码,因为它不能弄清楚在更大的范围内要做什么。但它可以为你优化基本的东西。

我一直认为,如果你在循环中声明变量,那么你就是在浪费内存。如果你有这样的东西:

for(;;) {
Object o = new Object();
}

然后,不仅需要为每次迭代创建对象,还需要为每个对象分配一个新的引用。看起来,如果垃圾收集器很慢,那么就会有一堆需要清理的悬空引用。

然而,如果你有这样的情况:

Object o;
for(;;) {
o = new Object();
}

然后,您只需创建一个引用,并每次为它分配一个新对象。当然,它可能需要更长的时间才能超出作用域,但这时只需要处理一个悬空引用。

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”。

好吧,你可以为它设定一个范围:

{ //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让我很恼火,因为它没有任何好处,而且它让我更难理解代码……

我的做法如下:

  • 如果变量类型是简单的(int, double,…),我更喜欢变量b(内部) 原因:减少变量的范围。< / p >

  • 如果变量类型不是简单的(某种__ABC0或struct)我更喜欢变量一个(外部) 原因:减少了ctor- doctor呼叫的数量

这是个有趣的问题。从我的经验来看,当你为代码争论这个问题时,有一个终极问题需要考虑:

为什么变量需要是全局的?

只全局声明一次变量而不是多次局部声明变量是有意义的,因为这样更有利于组织代码,需要的代码行更少。然而,如果它只需要在一个方法中局部声明,我会在该方法中初始化它,这样就可以清楚地看到变量只与该方法相关。如果您选择后一种选项,请注意不要在初始化该变量的方法之外调用该变量——您的代码将不知道您在谈论什么,并将报告错误。

另外,作为旁注,不要在不同的方法之间重复局部变量名,即使它们的目的几乎相同;这让人很困惑。

从性能的角度来看,外部(要好得多)。

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毫秒。

. in()耗时1.5秒

我做了一个简单的测试:

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编译了这些代码。然后我分解main () 这两个代码的结果是:

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

   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结果。这两种编码的结果不是一样的吗?

我用Node 4.0.0测试了JS,如果有人感兴趣的话。在循环外声明会导致~。平均超过1000次试验,每次试验1亿次循环,性能提高5毫秒。所以我要用最易读/可维护的方式来写,在我看来就是B。我本想把我的代码放在小提琴中,但我使用了现在性能的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)

这是更好的形式

double intermediateResult;
int i = byte.MinValue;


for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}

1)这样声明一次时间既是变量,而不是每个为周期。 2)任务比其他选项都要大。 3)所以最佳实践规则是迭代之外的任何声明。

在Go中尝试同样的事情,并使用go tool compile -S与Go 1.9.4比较编译器输出

零差异,根据汇编器输出。

很长一段时间我都有同样的问题。所以我测试了一段更简单的代码。

结论:对于这种情况下没有的性能差异。

外环箱

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);
}

我还使用回答中给出的方法反汇编了这两种情况的代码。我将只展示与答案相关的部分

外环箱

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;

如果你仔细注意,在LocalVariableTable中,只有分配给iintermediateResultSlot作为它们出现顺序的乘积被交换。slot中的相同差异也反映在其他代码行中。

  • 当前无额外操作
  • 在这两种情况下,intermediateResult仍然是一个局部变量,因此访问时间没有区别。

奖金

编译器做了大量的优化,看看在这种情况下发生了什么。

零工作情况

for(int i=0; i < 1000; i++){
int intermediateResult = i;
System.out.println(intermediateResult);
}

零工作反编译

for(int i = 0; i < 1000; ++i) {
System.out.println(i);
}

当我想在退出循环后查看变量的内容时,我使用(A)。它只与调试有关。当我希望代码更紧凑时,我使用(B),因为它节省了一行代码。