And IL disassemble for this
That's unoptimised code, typically produced by debug builds. Optimised code, typically produced by release builds would more likely be something like:
// Code size 10 (0xD) .maxstack 1 IL_0000: ldc.i4.1 IL_0001: box [mscorlib]System.Int32 IL_0006: unbox.any [mscorlib]System.Int32 IL_000B: pop IL_000C: ret
The two most obvious differences between your version and mine are:
- Mine doesn't have the
nop instruction. That "do nothing" instruction serves no purpose in the running code, but it does give a point to hang a breakpoint on in the IL if you put a breakpoint on the opening { of the C# you compiled from. - Yours does some busy-work storing and loading copies of variables that mine doesn't bother with.
(This isn't the most optimal version possible, incidentally).
It's important to consider that not only does the treatment of C# locals to IL locals vary according to types of build, but that the same thing also applies to the jitting stage.
There are even bigger differences when it comes to something like:
public static void Stuff() { int x = 2; Func<int> f = () => x * 2; }
Here x and f are both locals in the C#, but in the IL there's actually a heap object with a field and a method.
Local mean different things depending on the context, both as an adjective and a noun.
In C# local means method arguments and local variables within methods, both as a noun and an adjective. They are generally allocated on "the stack" (though in the case of reference types that stack allocated variable is referring to a heap-allocated object) for several meanings of "the stack" (we'll come to that later) but not always (captured locals and locals within yielding or awaiting methods that are maintained between calls [and sometimes when they aren't] are two examples). Most of the time we don't need to think too much about this fact, but the few times when we do can tend to lead to an overemphasis on the concept in how we talk about it.
In IL local as a noun refers to a set of strongly-typed locations initialised at the start of the method. As an adjective it refers to both those locals and to the locations on the stack that we push to and pop from (in IL we do need to think about the stack, a lot). These are both local locations, in so much as they can be thought of as "near by" but only one of them is generally referred to as locals when we're talking CIL. (If we were talking theory more generally we might call all of them locals, or not, depending on the point of view we were talking theory from).
But where does the locals really stored and where does they loaded from.
It depends on what you really mean by "really". But consider what the reason we use stacks for is anyway; it's a handy (but not the only) way to implement methods calling methods. You put some values on the stack, along with information about where you are now, and move into that method. Then when it is done it you have any return value on the stack and can do the next thing.
And the IL locals are a chunk of space after the arguments and before the chunk of stack you are working with, just as is reflected by the way the IL is laid out; arguments, locals, pushing and popping.
And that's sort of how things work in the actual machine too, as we can most easily seen when it doesn't work:
public static void Overflow() { Overflow(); Overflow(); }
Call that and we get a StackOverflowException (I have it call itself twice because then tail-call optimisation can't turn that exception into just never returning, which is just about possible). Which means an actual real chunk of memory being used as a stack got all used up.
And it won't be surprising that the actual stack and the IL stack have a definite relationship and so the method arguments, locals (in the IL noun sense) and the values pushed and popped can all relate to values stored in that chunk of memory.
But they can also be implemented as registers in the CPU, so a local might never be in memory at all.
And they might also not even be there. Consider my release-build version of your code. Actually, let's make it a full C# method:
public static void DoStuff() { int x = 1; object obj = x; int y = (int)obj; }
Now let's have something call it:
public static void CallDoStuff() { DoStuff(); }
So the compiler has turned DoStuff() into the code at the top of this answer, along with turning CallDoStuff() into:
call DoStuff ret
We run our application and come to where CallDoStuff() is first called, so the jitter has to compile it. It very likely sees that DoStuff() is very small and (along with a few other factors weighing on this decision) doesn't produce a function call at all but inlines all of those instructions into the code it's producing for CallDoStuff. Then it may see that the unboxed int (y) isn't used so it can leave that out, which means it can leave boxing the int out, which means it can leave producing the int out, which means we don't need any actual code for x, obj and y at all.
In which case the answer at that level as to where the values "really" are, is "nowhere".
asyncmethod or iterator block, how it's used (i.e. whether it even needs a storage location at all or if it can be optimized out of existence entirely, or perhaps only stored in registers), whether it's areforoutvariable, etc. Whether the value stored in the variable is a meaningful value or a reference to a value stored elsewhere has no impact on where it's stored.