Definitely, this is a very fun matter, it's nice to see how we have a lot of articles on the internet, talking about the difference between the stack and the heap memory, but 100% theory. I know what you are thinking, the theory is important and I agree, we are going to have it here too, but I also will show you things that your C# tips Guru didn't.
Boxing and Unboxing, What's is it?
There is no way to talk about stack and heap memories not mentioning boxing and unboxing. This is a totally related subject that we are going to cover a bit below, for now, before talking about it, we first need to understand value and reference types, which are the two main categories of C# types.
A C# value type variable stores a value, while a C# reference type variable doesn't, instead, it stores the reference to that value. This explanation was perfect, my goodness.
Quick reference, these are the values types: bool, decimal, sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double and enum. These are the reference types: dynamic, object, string, interface, object, delegate, class and array.
Connecting the points. In most cases, the value types are stored on the stack and the reference types are stored on the heap memory. Boxing means that you are converting a value type to a reference type, in other words, you are bringing data that is stored on the stack to the heap memory. Unboxing is the exact opposite. Boxing and unboxing are bad because you are exchanging data between memories, data that was already cached and in use by your application. This is not performative and it's important to remember that in the case of Boxing, you are also bringing more work to the Garbage Collector.
As you may know, or not, the Garbage Collector works on the heap memory, and every time that it's doing a collect, doesn't matter the generation (0, 1 or 2) it will stop your application - We can talk more about GC in another article.
When you are boxing (Stack -> Heap):
When you are unboxing (Heap -> Stack):
Yes! Boxing and unboxing are fancy terms for implicit and explicit casts between value and reference types. I'm feeling that you are understanding everything.
How to waste performance like a pro?
Warning: You should only use the code like below if you are really willing to impress someone with the worst practices. Here we have a really common (old) example:
ArrayList was created as an easier way of manipulating common arrays, as it's not necessary to worry about resizing, but the problem is that ArrayList is not strongly typed. It means that you can do like I did above, adding values of different types in the same array, and probably at the time that you need to use it, you will need to cast, consequently doing boxing and unboxing, which accordingly to Microsoft, it's a really expensive computing operation when compared with a simple assignment.
That's why Generic Lists (List<T>) were introduced. You still don't have to worry about resizing, but now you can use everything strongly typed, avoiding boxing and unboxing.
Debugging variables on the Stack memory
Let's go to the reason that you are still reading this article, the practical part. "SHOW ME THE CODE DUDE"- Here we go:
As any serious C# developer I use Rider, just kidding - maybe not. Anyway, above you can see that we have three int variables, as we learned int is a value type and value types are stored in the stack memory.
So, how can we see where those integers are allocated in the stack memory? Quite simple, use pointers as I did from line 5 to 10.
The most exciting part comes up now, we already have the address in the memory where those integers are stored (pointers addresses screenshot above - brown), now you need a tool capable of inspecting your computer memory. For this example, I'll use the HxD which is an hex editor, disk editor and memory editor - for windows, but there are also other tools.
To inspect the value of a memory address, you need to take three simple steps:
-
Keep your C# program running normally.
-
Open HxD and attach the memory inspector to your running code by clicking on Tools > Open main memory > Pick your C# program.
-
Select a memory block (any) and put the memory address (pointer address) that you want to inspect.
You will have something like this:
Now we need to get the pointers addresses, mine are 0x9c5e77e4f4, 0x9c5e77e4f0 and 0x9c5e77e4ec. Remembering that this is where the values of the variables stackMemoryValue01, stackMemoryValue02 and stackMemoryValue03 are respectively stored.
With the addresses that we want to inspect on hand, we just need to pick any memory block and paste our pointer address.
Notice that above after picking the block, I needed to put the initial and the final address that I want to inspect on the memory,
in this case, instead of using the final address field I used the Length, with the value of 4, this is because I want to read 4 bytes or 32 bits.
Observation: This is the size of the variable that we want to read as we are working with a signed int 32, different variable types
diferent Lengths.
Uow! This is Crazy! We just got the value of the variable stackMemoryValue01 directly from RAM memory!
Scroll above and take a look at the Rider screenshot, line code one, to confirm it. Try to inspect the other two variables
values by yourself.
Debugging variables on the Heap memory
Debugging heap memories it's really easier than the stack ones, at least if you are using Rider,
it already will show you the variables allocated on memory heap under the "Memory" tab.
Hoping you noticed that we are talking about the heap memory now, but I'm using the same code
that I used in the stack example.
I want to show you this boxing (int -> string). In the screenshot, I'm calling out attention to the debugger at line
12 and the Rider Memory Viewer. Check that until that moment the Memory Inspector console was empty-"Nothing to show".
Because until that moment there was no code allocated on the heap.
Now the breakpoint is at line 13, look at what happens when I do the boxing at line 12.
You can see that 3 more values (Diff column) were allocated at the moment that line 12 was executed.
The boxing, brought a value that was previously allocated on the stack memory to the heap.
Let's check:
Conclusion
This kind of understanding is neither trivial nor common, but it's super cool. You always need to remember, one thing is to study the basics another thing it's you study the bases. Hope you have enjoyed this little journey, don't forget to let your like.
References
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types#82-reference-types https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types#83-value-types