This is how it works…
You have reference types, and value types.
Reference types are most often classes and interfaces.
Value types are most often things like structs, primitives like int/float/bool, enums, etc.
Value types are said to usually exist “on the stack”… but that’s not exactly accurate. Value types exist in the memory scope that they are needed.
If it’s a variable in a function/method, then it exists on the stack (since the stack is where function/method memory exists). When a function/method is called, the stack expands with the necessary memory for said method. It’s like the first thing a method does, the stack has memory locations pushed onto the stack for every needed variable. Lets look at the IL code for Mathf.Min decompiled with Dotpeek:
/// <summary>
/// <para>Returns the smallest of two or more values.</para>
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="values"></param>
public static float Min(float a, float b)
{
return (double) a >= (double) b ? b : a;
}
//the IL for the above:
.method public hidebysig static float32
Min(
float32 a,
float32 b
) cil managed
{
.maxstack 2
.locals init (
[0] float32 V_0
)
IL_0000: nop
// [226 7 - 226 47]
IL_0001: ldarg.0 // a
IL_0002: ldarg.1 // b
IL_0003: bge.un IL_000e
IL_0008: ldarg.0 // a
IL_0009: br IL_000f
IL_000e: ldarg.1 // b
IL_000f: stloc.0 // V_0
IL_0010: br IL_0015
IL_0015: ldloc.0 // V_0
IL_0016: ret
} // end of method Mathf::Min
Note how the first thing called by the method is:
.maxstack 2
.locals init(
[0]float32 V_0
)
This essentially is telling the JIT compiler that the max size the stack needs is 2 words (32-bit) of memory.
This chunk of memory is what will be used by the method to operate during its life. Once the method is complete, that memory will be purged from the stack (by moving the stack pointer back maxsize words).
Any value types that exist as local variables (the 2 floats in the case of Min) will exist in this portion of memory. And this is very efficiently dealt with memory. There’s no garbage collection on it, it exists for the life of the method, and immediately is freed on completion. The overhead of creating structs like a Vector3 is if you happen to use a constructor method for said struct… which is itself another method call, which allocates on the stack as well before it returns (though most JIT compilers optimize these to some extent… but that’s getting into even nittier grittier junk you don’t have to concern yourself with).
…
THING IS, this doesn’t mean value types ALWAYS exist on the stack. Far from.
Take for instance this class:
public class Foo
{
public float value;
public Vector3 position;
}
Because it’s a class, it’s a reference type. And reference types are allocated on the heap. How this works is that the size of the object is determined, and that amount of memory is selected from the heap, and that is where the object will now exist. Any variable that references that object holds a pointer, the location in memory that the object exists (the pointer is smart though, since the object can move in the heap when the heap is resized/cleaned, the pointer may be updated behind the scenes… you don’t have to concern yourself with this).
The cost of this comes from how we remove objects from the heap.
Basically every once in a while the garbage collector sifts through the entire heap and checks if any references exist for an object. If no reference/pointer to an object exists, then it’s considered dead, and starts its deallocation process where it gets the deconstructor called (if one exists) and it is thrown out.
This process is expensive, especially in the version of Mono used by Unity.
Thing is… what exactly does this memory hold?
Well… in the case of our class Foo. It holds the fields/member variables that make up the class. In this case it’s a float and a Vector3. And since a Vector3 is just 3 floats… really the memory taken up is 4 32-bit words all identified as floats.
float|float|float|float
Along with the identifying nature of the object… which again is mostly invisible to you.
What this means is that YES, a value type or struct can exist on the heap. It just exists as the members of a class.
Another time it might happen is if you “box” your value type. This can happen if you say something like:
object x = 0.1f;
Or if say you have a struct that implements an interface. And you then reference that struct via its interface:
public struct Bar : IEnumerator
{
public float A;
public float B;
//implement IEnumerator to enumerator A and B
}
IEnumerator e = new Bar() {
A = 1f,
B = 5f
};
both x and e in these examples are boxed an on the heap.
…
So in your initial code:
IEnumerator UpdatePath()
{
pathfinder.SetDestination(new Vector3(target.position.x, 0, target.position.z));
yield return new WaitForSeconds(0.1);
}
Well every time you call UpdatePath, a ‘new IEnumerator’ is returned (this is called an iterator function and is syntax sugar for a enumerator that steps through your code… this can be very complicated under the hood). You also create a ‘new WaitForSeconds’ object.
But as for the variables… they exist on the sta… wait, no they don’t.
So, an iterator function really is syntax sugar for an anonymous object.
Really, the compiler turns this code into a class. All the variables in the function instead of being stack members like I described in the IL above, they are fields/member variables of this anonymous class. The UpdatePath function now returns a ‘new’ object that is the type of this anonymous class. And that object exists on the heap.
This object implements the IEnumerator interface, and the MoveNext method is implemented to move through the code to each ‘yield’ statement. It unravels to look like an IL equivalent of a select statement, and a state field that tracks at what point in the code you’re at.
It’s not the Vector3 that costs anything… it’s the creating this object that costs stuff. And that cost would be there regardless of the Vector3 or not.