Value types and reference types

Not all memory allocations we make within Mono will go through the heap. The .NET Framework (and, by extension, the C# language, which merely implements the .NET specification) has the concept of value types and reference types, and only the latter needs to be marked by the GC while it is performing its Mark-and-Sweep algorithm. Reference types are expected to (or need to) last a long time in memory due to their complexity, their size, or how they're used. Large datasets and any kind of object instantiated from a class instance is a reference type. This also includes arrays (regardless of whether it is an array of Value types or reference types), delegates, all classes, such as MonoBehaviour, GameObject, and any custom classes we define.

Reference types are always allocated on the heap, whereas value types can be allocated either on the stack or the heap. Primitive data types such as bool, int, and float are examples of value types. These values are typically allocated on the stack, but as soon as a value type is contained within a reference type, such as class or an array, then it is implied that it is either too large for the stack or will need to survive longer than the current scope and must be allocated on the heap, bundled with the reference type it is contained within.

All of this can be best explained through examples. The following code will create an integer as a value type that exists on the stack only temporarily:

public class TestComponent {
void TestFunction() {
int data = 5; // allocated on the stack
DoSomething(data);
} // integer is deallocated from the stack here
}

As soon as the TestFunction() method ends, the integer is deallocated from the stack. This is essentially a free operation since, as mentioned previously, it doesn't bother doing any cleanup; it just moves the stack pointer back to the previous memory location in the call stack (back to whichever function called TestFunction() on the TestComponent object). Any future stack allocations simply overwrite the old data. More importantly, no heap allocation took place to create the data, so the GC does not need to track its existence.

However, if we created an integer as a member variable of the MonoBehaviour class definition, then it is now contained within a reference type (class) and must be allocated on the heap along with its container:

public class TestComponent : MonoBehaviour {
private int _data = 5;
void TestFunction() {
DoSomething(_data);
}
}

The _data integer is now an additional piece of data that consumes space in the heap alongside the TestComponent object it is contained within. If TestComponent is destroyed, then the integer is deallocated along with it, but not before then.

Similarly, if we put the integer into a normal C# class, then the rules for reference types still apply and the object is allocated on the heap:

public class TestData {
public int data = 5;
}

public class TestComponent {
void TestFunction() {
TestData dataObj = new TestData(); // allocated on the heap
DoSomething(dataObj.data);
} // dataObj is not immediately deallocated here, but it will
// become a candidate during the next GC sweep
}

So, there is a big difference between creating a temporary value type within a class method versus storing long-term value type as a member field of class. In the former case, we're storing it in the stack, but in the latter case, we're storing it within a reference type, which means it can be referenced elsewhere. For example, imagine that DoSomething() has stored the reference to dataObj within a member variable:

public class TestComponent {
private TestData _testDataObj;

void TestFunction() {
TestData dataObj = new TestData(); // allocated on the heap
DoSomething(dataObj.data);
}

void DoSomething (TestData dataObj) {
_testDataObj = dataObj; // a new reference created! The referenced
// object will now be marked during Mark-and-Sweep
}
}

In this case, we would not be able to deallocate the object pointed to dataObj as soon as the TestFunction() method ends because the total number of things referencing the object would go from 2 to 1. This is not 0, and hence the GC would still mark it during Mark-and-Sweep. We would need to set the value of _testDataObj to null or make it reference something else before the object is no longer reachable.

Note that a value type must have a value and can never be null. If a stack-allocated value type is assigned to a reference type, then the data is simply copied. This is true even for arrays of value types:

public class TestClass {
private int[] _intArray = new int[1000]; // Reference type
// full of Value types
void StoreANumber(int num) {
_intArray[0] = num; // store a Value within the array
}
}

When the initial array is created (during object initialization), 1000 integers will be allocated on the heap set to a value of 0. When the StoreANumber() method is called, the value of num is merely copied into the zeroth element of the array rather than storing a reference to it.

The subtle change in the referencing capability is what ultimately decides whether something is a reference type or a value type, and we should try to use value types whenever we have the opportunity so that they generate stack allocations instead of heap allocations. Any situation where we're just sending around a piece of data that doesn't need to live longer than the current scope is a good opportunity to use a value type instead of a reference type. Ostensibly, it does not matter if we pass the data into another method of the same class or a method of another class—it still remains a value type that will exist on the stack until the method that created it goes out of the scope.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.137.164