One thing to keep in mind is that local variables are typically allocated on the stack. One task that a compiler must do is figure out how much stack space a particular method requires and set that aside.
Consider:
int Func(int a, int b, int c)
{
int x = a * 2;
int y = b * 3;
int z = c * 4;
return x + y + z;
}
Ignoring the fact that this can be easily optimized to be return (a * 2) + (b * 3) + (c * 4), the compiler is going to see three local variables and set aside room for three local variables.
If I have this:
int Func(int a, int b, int c)
{
int x = a * 2;
{
int y = b * 3;
{
int z = c * 4;
{
return x + y + z;
}
}
}
}
It's still the same 3 local variables - just in different scopes. A for loop is nothing but a scope block with a little glue code to make it work.
Now consider this:
int Func(int a, int b, int c)
{
int x = a * 2;
{
int y = b * 3;
x += y;
}
{
int z = c * 4;
x += z;
}
return x;
}
This is the only case where it could be different. You have variables y and z which go in and out of scope - once they are out of scope, the stack space is no longer needed. The compiler could choose to reuse those slots such that y and z share the same space. As optimizations go, it's simple but it doesn't gain much - it saves some space, which might be important on embedded systems, but not in most .NET applications.
As a side note, the C# compiler in VS2008 in release isn't even performing the simplest strength reductions. The IL for the first version is this:
L_0000: ldarg.0
L_0001: ldc.i4.2
L_0002: mul
L_0003: stloc.0
L_0004: ldarg.1
L_0005: ldc.i4.3
L_0006: mul
L_0007: stloc.1
L_0008: ldarg.2
L_0009: ldc.i4.4
L_000a: mul
L_000b: stloc.2
L_000c: ldloc.0
L_000d: ldloc.1
L_000e: add
L_000f: ldloc.2
L_0010: add
L_0011: ret
whereas, I fully expected to see this:
L_0000: ldarg.0
L_0001: ldc.i4.2
L_0002: mul
L_0003: ldarg.1
L_0004: ldc.i4.3
L_0005: mul
L_0006: add
L_0007: ldarg.2
L_0008: ldc.i4.4
L_0009: mul
L_000a: add
L_000b: ret