by kirupa |
29 March 2007All of the tutorials on this site
cover high level languages such as C#, AtionScript, PHP,
etc. One of the things that make a language a higher-level
language is the amount of detail you as a programmer do not
have to bother with. Behind the scenes of your cleanly
written code, a compiler goes through and does a lot of
complicated work.
Most of the time, knowing what the compiler does behind
the scene is not important. But, there will be situations
where having a basic idea of what goes on behind your code
can either help you to improve your code or debug tricky
issues. In this article, I am going to talk about stacks and
heaps. They are both play a large role in running your
applications, and while all of the code snippets in the
following pages will be C#, you can apply the concepts to
almost all languages.
A program you run contains code that designates
variables, methods/functions, etc. All of those various bits
and pieces are simplified into smaller pieces so that your
processor can execute them in some particular order. The
exact location where your code is simplified and stored can
be found in two places, the stack and the heap.
Before delving into the details of stacks and
heaps, I must first explain the two
types of data you use in .NET - Value and
Reference. Value types are often primitives, and as
their name implies, store data values in the form of int,
double, struct, etc. More specifically, any data type that is
based on System.ValueType is a value type. Reference types
on the other hand are based on System.Object, and a popular
example of a reference type is a
Class!
Knowing about value and reference types is important,
because that classification determines where in memory they
are stored. There are two main regions of memory where
programs you run are kept: the stack and the heap. Value
types are usually stored on the stack, and reference types
are stored on the heap.
Let's look at stacks first. A stack is
known as LIFO (last-in, first-out) data structure that works
similar to you stacking blocks on top of each other and
trying to access a random block. For example, take a look at
the following diagram with three stages displayed:
The most recent block I added is the red one (Stage 1).
If I want to access the green block, I need to first remove
the red block (Stage 2), and then I need to remove the blue
block (Stage 3). This is an example of LIFO where the last
block added is the first block to removed - which in our
case are the red and blue blocks.
Now, let's expand our understanding of stacks in the
computer world by looking at a simple program that
calculates a circumference given a radius:
- public
double getArea(int
radius)
{
- const
double
PI
= 3.14;
- double
area
= PI
*
radius * radius;
- return
area;
- }
When you run this program, your stack will grow as shown
in the following diagram:
In Stages 1 and 2, our method and its parameter are put
on the stack. In Stage 3, we allocate space for our PI
variable, and in Stage 4, we add some space for our area
variable. At the end of Step 4, our program has fully
executed, and all of the variables that needed to reserve
space on the stack have been taken care of. Once you are
done executing the code, you clear the stack as shown in
Stage 5 and return back to the point in the memory where
getArea() once resided!
In practice, your stack also has various pointers that
store memory addresses. In the above image, the pink arrows
designate a stack pointer that indicates the top of the
stack. You also have pointers that tell a subroutine where
to return to.
If I were talking about a lower level language, then I
would have to elaborate more on safe coding practices. For
example, a recursive function can inflate the stack very
quickly because the stack will end up storing the return
location for each recursive function call. The solution is
to modify your function to work iteratively. In an iterative
solution, you are directly modifying the result
of a previous function call instead of storing a
collection of results from previous
function call and saving any calculations until the end.
Luckily, such optimizations are done automatically for us,
and you can learn more about that by learning more about
tail-recursion [wikipedia].
In the
previous post, I gave a brief overview of
what stacks do. In this post, I will pick up from where
I left off about the interesting world of heaps. As you
will see, heaps are not independent of stacks, so if you
have glanced over my earlier post, it is a good idea to
do so.
Heaps, like I mentioned in my previous post, deal
primarily with reference types, and one popular
reference type that you may already be familiar with is
a class. To best explain how the heap
works, let me first show you an example:
- class
Simpsons
- {
- public
string
name;
- public
int
age;
- }
-
- public
void
People()
- {
- Simpsons
bart
=
new
Simpsons();
- bart.name
=
"Bart";
- bart.age
=
10;
- }
The above code has a class called Simpsons, and the
Simpsons class has two public fields called name
and age. You access the Simpsons class
via your People method, and inside your People
method, you both create a bart object of type Simspons
and initializie the bart object's name
and age fields.
Now that you an idea of what the code does, let's
take a look at how both our stack and heap look like at
the end of running the People method:
Your stack, like before, contains the People
method call, and it includes an entry for bart. Notice
that bart isn't pointing to a value,
but instead it is referencing
the Simpsons object on the heap. The reason is that
Simpsons is a class aka a reference type, and like
I mentioned before, reference types usually belong on
the heap.
Garbage Collection
After your People method has finished running, your
stack would once again be empty since it only contains
information on code that is or will be running. With
nothing left to run, our stack will be empty. The heap,
on the other hand, can still contain data:
In such a situation, you will
have something called a Garbage Collector that
goes through and clears unreferenced/unused data from
the heap. For example, the Simpsons class above would be
removed because it is no longer being used, and you will
end up with an empty stack and heap. I should clarify
that in most scenarios, when your application is more
complicated than the example presented in this post, you
may have some data from other methods taking up space in
both your stack and heap.
There are numerous approaches garbage collectors take
when attempting to optimize free space, and that depends
on your language and runtime environment. Visual Basic 6
and earlier used a reference counting approach where you
count the number of references each object on the heap
has pointing to it, but (as far as I know) .NET uses a reference
tracing approach. Most algorithms that attempt reference tracing build a
tree of all objects based on entry points in your
application called roots. Any objects that don't make it
onto the tree are assumed to be wasted space, so they
are quickly cleaned up.
The actual GC implementation in the .NET
VM's may go several steps beyond what I explained, and
there are various topics that I will not cover in this
article such as dealing with fragmentation or how objects
get prioritized for removal. If you are curious to learn
more about these topics, the following
course notes may come in handy.
Using the = Operator with Reference Types
With value types, when you set one variable equal to
another variable, the actual data gets copied. For
example, take a look at the following code:
- static
void
Main()
- {
- int
x
=
10;
- int
y
=
x;
-
- x
=
5;
-
- // x = 5
- // y = 10
- }
I set the y variable equal to x, and I then change
the value of x to be 5. In the end, the values stored by
x is 5 and the value stored by y is 10. This is just
what we want! With reference types, though, setting one
variable equal to another variable only transfers the
reference to the same object
on the heap.
Let's look at a demo of what I am referring to:
- class
Simpsons
- {
- public
string
name;
- public
int
age;
- }
-
- public
void
People()
- {
- Simpsons
bart
=
new
Simpsons();
- Simpsons
bort
=
bart;
-
- bart.name
=
"Bart";
- bart.age
=
11;
-
- bort.name
=
"Bort";
- bort.age
=
22;
- }
In the above example, I create two objects called
bort and bart. The thing to note is that the bort object
is set equal to the bart object, and I then proceed to
populate the fields in the bort and bart objects. When I
Console.WriteLine bart's name as in bart.name, what is
the output you get? What should be the output you want?
The output for bart.name will be Bort.
The output you would have wanted would have been
Bart. The reason for this goes back to the
difference between value and reference types that I
briefly mentioned earlier. Let's look at both the stack
and heap for the above example:
Do you see the issue? Both the bort and bart objects
are referencing the same thing! By setting bort = bart,
what I essentially did was set the bort reference equal
to the bart reference. That brings us full-circle
back to a
simple fact of programming in .NET and most other
languages: reference types store references
and value types store values!
Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.
Your support keeps this site going! 😇

|