A for loop does not have a separate stack frame from the method in which it exists.
If you take similar code like this:
private static void FooA()
{
StructA v;
int result = 0;
for(int i = 0; i < 1000000; i++)
{
v = new StructA()
{
A = i,
B = i * 2
};
result += v.A + v.B;
}
Console.WriteLine(result);
}
private static void FooB()
{
int result = 0;
for (int i = 0; i < 1000000; i++)
{
StructA v = new StructA()
{
A = i,
B = i * 2
};
result += v.A + v.B;
}
Console.WriteLine(result);
}
And look at itâs IL, youâll see something like this for the first part of each method (note, Iâm using Visual Studio 2015 for this, but as youâll see, the version doesnât particularly matter here because of the way the stack works on a fundamental level with mono/.net):
FooA
.method private hidebysig static void
FooA() cil managed
{
.maxstack 3
.locals init (
[0] valuetype Console01.StructA v,
[1] int32 result,
[2] int32 i,
[3] valuetype Console01.StructA V_3,
[4] bool V_4
)
FooB
.method private hidebysig static void
FooB() cil managed
{
.maxstack 3
.locals init (
[0] int32 result,
[1] int32 i,
[2] valuetype Console01.StructA v,
[3] valuetype Console01.StructA V_3,
[4] bool V_4
)
Note how the IL for both starts off relatively the same.
1. it defines the max stack size, this is the maximum number of pushes onto the stack that may exist at any time for the life of this stack frame (the methods life time). In this case because my code every really has 3 substintive variables that have coincidental usage (they have meaningful value at the same time)⌠that means we only need a stack frame of that size (3) to work with.
To help with what I mean, if I had say:
private static int Bar(int i)
{
if(i < 0)
{
int j = i;
j += 7;
i = j + i;
}
else
{
float k = (float)i;
k -= 6.9f;
i = (int)(k * i);
}
return i;
}
This has 3 variables: i,j,k; of varying types. BUT the IL reads a max stack size of 2:
.method private hidebysig static int32
Bar(
int32 i
) cil managed
{
.maxstack 2
.locals init (
[0] bool V_0,
[1] int32 j,
[2] float32 k,
[3] int32 V_3
)
This is because only 2 variables are needed at any given moment. i is needed for the life, j is only needed if i is negative, and k is only needed if i is zero/positive (despite difference in type). So only 2 variables have any coincidence.
Moving onâŚ
2. the IL calls âinit localâ.
This is declaring names of variables in this method. You may also notice a handful of variables that arenât in my code⌠these are implicit variables that would have to exist due to the structure of my code. Such as the boolean used in the resolving the for loop.
As you can see though⌠THIS is when the variables are actually initialized. And itâs always this way. Itâs the first thing done in the method⌠regardless of where the variable is used in the function.
If you have ever written any older languages, you may have tripped over languages that have this built into their syntax. Where all variables in a method had to be declared first and foremost⌠forcing the programmer to syntactically do this because the compiler was too simple to look through the method, determine what variables exist, rewind, so on so forth.
And of course⌠this goes for IL as well⌠IL is intermediate code. The JIT compiler is what ends up compiling this IL into machine code⌠and to speed things along, the IL puts this up front to speed of compilation by the JIT. BUT the C#->IL compiler can do this for us, foregoing the programmer having to do it.
The only big difference with the 2 is that ORDER of the variables in the init local⌠because they show up in our method in different orders.
âŚ
If we continue on with the code, youâll notice theyâre roughly identical, the only differences really being the index of the variable in the IL because the order in âinit localâ is different:
FooA
.method private hidebysig static void
FooA() cil managed
{
.maxstack 3
.locals init (
[0] valuetype Console01.StructA v,
[1] int32 result,
[2] int32 i,
[3] valuetype Console01.StructA V_3,
[4] bool V_4
)
// [21 9 - 21 10]
IL_0000: nop
// [23 13 - 23 28]
IL_0001: ldc.i4.0
IL_0002: stloc.1 // result
// [25 17 - 25 26]
IL_0003: ldc.i4.0
IL_0004: stloc.2 // i
IL_0005: br.s IL_0039
// start of loop, entry point: IL_0039
// [26 13 - 26 14]
IL_0007: nop
// [27 17 - 31 19]
IL_0008: ldloca.s V_3
IL_000a: initobj Console01.StructA
IL_0010: ldloca.s V_3
IL_0012: ldloc.2 // i
IL_0013: stfld int32 Console01.StructA::A
IL_0018: ldloca.s V_3
IL_001a: ldloc.2 // i
IL_001b: ldc.i4.2
IL_001c: mul
IL_001d: stfld int32 Console01.StructA::B
IL_0022: ldloc.3 // V_3
IL_0023: stloc.0 // v
// [33 17 - 33 37]
IL_0024: ldloc.1 // result
IL_0025: ldloc.0 // v
IL_0026: ldfld int32 Console01.StructA::A
IL_002b: ldloc.0 // v
IL_002c: ldfld int32 Console01.StructA::B
IL_0031: add
IL_0032: add
IL_0033: stloc.1 // result
// [34 13 - 34 14]
IL_0034: nop
// [25 41 - 25 44]
IL_0035: ldloc.2 // i
IL_0036: ldc.i4.1
IL_0037: add
IL_0038: stloc.2 // i
// [25 28 - 25 39]
IL_0039: ldloc.2 // i
IL_003a: ldc.i4 1000000 // 0x000f4240
IL_003f: clt
IL_0041: stloc.s V_4
IL_0043: ldloc.s V_4
IL_0045: brtrue.s IL_0007
// end of loop
// [36 13 - 36 39]
IL_0047: ldloc.1 // result
IL_0048: call void [mscorlib]System.Console::WriteLine(int32)
IL_004d: nop
// [37 9 - 37 10]
IL_004e: ret
} // end of method Program::FooA
FooB
.method private hidebysig static void
FooB() cil managed
{
.maxstack 3
.locals init (
[0] int32 result,
[1] int32 i,
[2] valuetype Console01.StructA v,
[3] valuetype Console01.StructA V_3,
[4] bool V_4
)
// [40 9 - 40 10]
IL_0000: nop
// [41 13 - 41 28]
IL_0001: ldc.i4.0
IL_0002: stloc.0 // result
// [43 18 - 43 27]
IL_0003: ldc.i4.0
IL_0004: stloc.1 // i
IL_0005: br.s IL_0039
// start of loop, entry point: IL_0039
// [44 13 - 44 14]
IL_0007: nop
// [45 17 - 49 19]
IL_0008: ldloca.s V_3
IL_000a: initobj Console01.StructA
IL_0010: ldloca.s V_3
IL_0012: ldloc.1 // i
IL_0013: stfld int32 Console01.StructA::A
IL_0018: ldloca.s V_3
IL_001a: ldloc.1 // i
IL_001b: ldc.i4.2
IL_001c: mul
IL_001d: stfld int32 Console01.StructA::B
IL_0022: ldloc.3 // V_3
IL_0023: stloc.2 // v
// [51 17 - 51 37]
IL_0024: ldloc.0 // result
IL_0025: ldloc.2 // v
IL_0026: ldfld int32 Console01.StructA::A
IL_002b: ldloc.2 // v
IL_002c: ldfld int32 Console01.StructA::B
IL_0031: add
IL_0032: add
IL_0033: stloc.0 // result
// [52 13 - 52 14]
IL_0034: nop
// [43 42 - 43 45]
IL_0035: ldloc.1 // i
IL_0036: ldc.i4.1
IL_0037: add
IL_0038: stloc.1 // i
// [43 29 - 43 40]
IL_0039: ldloc.1 // i
IL_003a: ldc.i4 1000000 // 0x000f4240
IL_003f: clt
IL_0041: stloc.s V_4
IL_0043: ldloc.s V_4
IL_0045: brtrue.s IL_0007
// end of loop
// [54 13 - 54 39]
IL_0047: ldloc.0 // result
IL_0048: call void [mscorlib]System.Console::WriteLine(int32)
IL_004d: nop
// [55 9 - 55 10]
IL_004e: ret
} // end of method Program::FooB
SO⌠no where you define the variable doesnât matter so much with the initializing of the variable. Itâs initialized up front no matter what.
BUT this comes with a different implication.
Lets take this code. This is a modified version of the âBarâ earlier in this post. This time though I named the variable j in both scopes of the if statement. And typed them both âintâ.
private static int BarB(int i)
{
if (i < 0)
{
int j = i;
j += 7;
i = j + i;
}
else
{
int j = i;
j -= 7;
i = j * i;
}
return i;
}
Youâd think that the the compiler would consider this 1 variable really⌠but nope:
.method private hidebysig static int32
BarB(
int32 i
) cil managed
{
.maxstack 2
.locals init (
[0] bool V_0,
[1] int32 j,
[2] int32 j,
[3] int32 V_3
)
Because really, IL doesnât care about the ânameâ. Itâs just an index. And because there is 2 variables in 2 different scopes, we end up with 2 distinct variables.
So⌠hopefully that clears up some things for ya.