Original optimizing garbage collection in Unity games. Here is a rough translation of this article, as your own notes.
Garbage collector is translated into a noun GC here. For garbage collection This translates into a verb memory recovery.
Memory garbage: The code has been destroyed (disposed) but the GC has not yet cleaned up the memory.
A simple introduction to Unity's managed memory
To understand how GC (this GC refers to garbage Collector) works in memory allocation and recycling, we must first understand how Unity's engine code and the scripts we write ourselves use memory to work.
The way the Unity Engine code runs is to manage memory manually (manual memory management). This means that the engine code must display the declaration of how memory is used. Manual management of memory is not used in GC, this section of this article does not introduce.
The way Unity manages this part of memory when running our own scripts is called Automatic memory management (automatic management). This means that the code we write ourselves does not need to be detailed to tell unity how to manage this part of the memory. Unity has done it for us.
In short, Unity's automatic management of memory works as follows:
- Unity has two memory pools that can be accessed: stack (stack) and heap (heap) (also called the managed heap). Stack memory is used for short-term storage of some small fragments of data, and the heap is used to store some long-term and larger fragment data.
- When a variable is created, unity will request a block of memory on the stack or heap.
- As long as this created variable is within the scope of the action (our code can always access it), the corresponding block of memory allocated to it is always available. We make this memory an already allocated (allocated). We call the variable that allocates memory in the stack called the object on the stack, and the variable that allocates memory on the heap becomes the object on the heap.
- When the variable is out of its scope, the memory allocated by the corresponding variable is no longer needed, and the memory is then returned to the allocated memory pool. We call this memory called recycled (deallocated). The memory on the stack is immediately recycled once it leaves the function area. If the memory on the heap is not immediately recycled even if its reference has gone beyond the scope of the action, it will also maintain its allocation state.
- The GC flags and reclaims memory that is not used on the heap. It periodically cleans up the memory on the heap.
Now that we understand the flow of memory usage, let's take a deeper look at the allocation and deallocation of memory on stacks and heaps.
What happens when the stack allocates and frees up memory?
Stack memory allocation and recycling is quick and easy. This is because the stack is only used to store small fragments of short periods of data. The operations of allocations and recoveries occur essentially in the desired order and size (amount of memory).
The stack works like a stack of data structure types: It is a simple element collector, in which case the stack of memory blocks can only be added and removed in strict order. Simple and strict rules make this operation very fast, when a variable needs to be stored on the stack, memory will simply allocate a piece of memory for it on the bottom of the stack. When a stack of variables has gone out of scope, the memory that stores the variable is immediately released into usable memory.
What happens when the heap allocates and frees up memory?
The heap's memory allocation is much more responsible than the stack's memory allocation. This is because the heap can be used to store long-period data as well as to store short-period data. And these data can be of various different types and different sizes. Allocations and collections do not occur frequently in the order expected, and the memory allocated and reclaimed may be large chunks of memory that differ in size.
When a variable of heap memory is created, it executes these steps sequentially:
- First, unity must check if there is enough remaining memory on the heap that can be allocated. If sufficient, the variable will be allocated a corresponding size of memory
- If the memory on the heap is not enough to allocate the memory required for this variable. Unity's GC triggers attempt to free memory that is not used on the heap. This is going to be a slow operation. If there is enough remaining memory after the release, the variable is allocated a corresponding size of memory.
- Unity will increase the size of the heap memory if the remaining memory on the heap after the GC frees memory is still insufficient for the memory required by this variable. This is also a relatively slow operation. This will be allocated to the corresponding size of the memory space after this operation.
Heap memory allocation is slow, especially for the expansion of GC run and heap memory.
What happens when a GC does a memory recovery?
When a heap memory variable is beyond its scope (or life cycle), the memory that stores the variable is not immediately recycled, and this memory can be called unused memory. Unused memory is recycled only when the GC is running.
Every time the GC runs, it executes the steps in sequence:
- The GC examines each object on the heap
- The GC finds references to all current objects to determine whether the object is still within scope.
- Any object that is not within the scope of the action is flagged by the GC as an object that needs to be deleted.
- Delete the tagged objects and then reclaim the memory that these variables occupy.
GC Recycling of memory is a relatively expensive operation. The more objects on the heap, the more references to objects, and the more work that the GC will need to deal with.
What will cause the GC to run
There are three scenarios that can cause the GC to run:
- Whenever a heap of memory does not meet the user's requested memory.
- The GC performs its own unscheduled execution (this frequency is determined by the platform).
- When a user actively forces a GC collection to be invoked
GC reclaims memory is a more frequent operation. Whenever the heap memory does not meet the size of the memory that the variable requests, the GC Reclaim memory operation will be triggered, which also means that frequent heap memory requests and release operations will cause the GC to run frequently.
Issues with GC Reclaim memory
Now that we've learned about the role of Unity's memory management in GC recycling, we can consider a variety of issues that might occur when the GC reclaims memory.
One obvious problem is that the GC spends a lot of time. If the GC needs to examine a large number of heap memory objects or a large number of object reference relationships, the process of checking will be very slow, which will also cause our game to stutter (stutter) or run slowly.
Another problem is that the GC may be executed at an inappropriate time. If the CPU is already burdened with a very heavy load on the performance critical part of our game, this time, even a small extra burden on the CPU will cause our game's frame rate to drop and the performance will be significantly worse.
A less obvious problem is fragmentation of heap memory. When allocating memory from the heap, it is necessary to allocate from the remaining memory blocks according to the size of the stored data, and when the allocated memory is reclaimed, the heap memory will be massively divided into many small contiguous blocks of memory available. This means that the GC does not have a memory recycle and that the total amount of available memory is much more, but we cannot allocate a larger piece of contiguous memory.
Here we summarize the problems that two memory fragmentation will cause. On the one hand our game memory usage is higher than we need, on the other hand GC memory recycling operations will be more frequent. To get a more detailed discussion of heap memory fragmentation, you can read this article on the Unity Best Practice Guide on performance
Finding the allocation point for heap memory
If we know what causes the GC to run in our game, we need to know which part of the code generated the memory garbage. The memory garbage is generated from the heap memory variables that are out of scope, so we first need to know why the variables allocate memory in the heap.
The code listed below is an example of allocating memory on the stack, where the variable localint is local and also a value type variable (value-type) the memory of the variable will be immediately recycled into the stack after the function is run.
void ExampleFunction(){ int localInt = 5;}
The code listed below is an example of allocating memory on a heap, where the variable locallist is a local variable but is a reference type (Reference-type). The memory recycle of this variable is when the GC is running (the function is not released immediately after execution).
void ExampleFunction(){ List localList = new List();}
Use Unity's profile window to find the allocation of heap memory
We can see where our code creates heap memory through the profiler window
In the CPU Usage analyzer, we can select any frame and then view the CPU data usage for this frame in the bottom part of the profiler window, where a column of data is called GC Alloc . This column shows the memory allocations that are in progress on this frame. If we choose the head of this column we can sort these statistically good data to make it easier to see which functions in our game generate the largest heap memory allocations. Once we know which function generated the allocated heap memory, we can check this function.
When we know the code that causes memory allocation in this function, we can decide how to solve this problem so that the amount of memory allocated here is minimized.
Reduce the impact of GC reclaim memory
In summary, there are three ways to reduce the impact of GC recovery memory for games:
- We can reduce the time the GC runs
- We can reduce the frequency of a GC run
- We can carefully trigger GC reclaim memory when the performance pressure ratio is small, for example in the loading interface
Based on these ideas, here are three strategies to help us:
- We can construct our game so that we have fewer heap memory allocations and fewer object references. Fewer objects on the heap and fewer object references to check mean that GC reclaims memory takes less time when the GC is triggered.
- We can reduce the amount of heap memory allocation and release frequency, especially when the performance pressure is relatively large. Fewer memory allocations and releases will cause fewer scenes to trigger GC for garbage collection. This also has less risk of heap memory fragmentation
- We can try to expand the memory by proactively invoking GC garbage collection as expected at a convenient time. This is difficult and difficult to achieve, but can reduce the impact of GC garbage collection When this approach is part of the overall memory management strategy.
Reduce the amount of junk memory creation
Let's do some technical testing to help us reduce the amount of junk memory generated in our own code.
Cache
If our code calls a function repeatedly, this function causes the heap memory allocation and discards the result of the function every time we call the function, creating unnecessary memory garbage. Instead, we should store these objects for reuse. This technique is called caching.
In the example listed below, this code generates heap memory allocations each time it is called. This is because a new array is created.
void OnTriggerEnter(Collider other){ Renderer[] allRenderers = FindObjectsOfType<Renderer>() ExampleFunction(allRenderers);}
The code listed below is modified, and now no matter how many times the call to Ontriggerenter will only cause a heap memory allocation, the array is created and assigned and then cached. This cache array can be reused and does not generate additional memory.
private Renderer[] allRenderers;void start(){ allRenderes = FindObjectsOfType<Renderer>() }void OnTriggerEnter(Collider other){ ExampleFunction(allRenderers);}
Do not allocate memory in frequently called functions
If we have to allocate heap memory in the Monobehaviour script, the worst thing is that we put this operation in a function that is frequently run. For example, Update () and lateupdate, these two functions are called each frame. So if we allocate the heap memory code in this function, the memory is increased very quickly. In this case, let's consider making this object either created in the start () or awake () function and used to save the object, or make sure that the code allocates memory only when it is needed.
Let's look at a very simple example, and this example changes slightly so that the function runs only when the condition changes. The code listed below, a function that generates memory allocations, is called by update () every frame, creating memory garbage frequently.
void Update(){ ExampleGarbageGeneratingFunction(transform.position.x);}
To make a simple change, we now make sure to call examplegarbagegeneratingfunction only if the value of transform.position.x is changed. After the change, the memory will be allocated only when we need it, not every frame will be allocated once.
private float previousTransformPositionX;void Update(){ float transformPositionX = transform.position.x; if (transformPositionX != previousTransformPositionX) { ExampleGarbageGeneratingFunction(transformPositionX); previousTransformPositionX = transformPositionX; }}
Another way to reduce the amount of memory garbage generated is to use a timer inside the update (). This method is more suitable for our function of generating memory garbage examplegarbagegeneratingfunction must run regularly, but it does not need to be as frequent as every frame.
The code in the following example, the function examplegarbagegeneratingfunction will be called every frame, this function will generate memory garbage inside.
void Update(){ ExampleGarbageGeneratingFunction();}
We use a timer to modify this code to make sure that the function that generates the memory garbage is called once per second.
private float timeSinceLastCalled;private float delay = 1f;void Update(){ timeSinceLastCalled += Time.deltaTime; if (timeSinceLastCalled > delay) { ExampleGarbageGeneratingFunction(); timeSinceLastCalled = 0f; }}
When our code is running very often, a small change can significantly reduce the amount of memory garbage.
Cleaning containers
Creating a new container (List, dictionary, and so on) will request the allocation of memory on the heap. If we find that there are more than one new container created in the code, then we should cache this reference to the container and then use Clear () to empty the container instead of calling new again to recreate it.
In the following example, each call to the new keyword causes the heap memory to be allocated once.
void Update(){ List myList = new List(); PopulateList(myList);}
In the following example, memory allocation occurs only when the container is created or the container must change the storage size. This can significantly reduce the amount of memory waste generated.
private List myList = new List();void Update(){ myList.Clear(); PopulateList(myList);}
Object Pool
Even if we reduce the memory allocations in our scripts, if we create and destroy objects in large numbers while the game is running, then the issue of GC reclaim memory remains. For reused objects, it is not necessary for us to create and destroy them every time we use them, using the object pooling to reduce the allocation and deallocation of memory for reusable objects. Object pooling is widely used in games where small objects are frequently generated and destroyed. For example, bullets fired from a gun.
This article does not discuss the complete use of the object pooling specification, but this is a very useful and worthwhile technique to learn. This tutorial on object pooling on the unity learn site is a very good implementation specification for unity's object pooling system.
Common causes of unnecessary heap memory allocations
We have understood that a variable of value type is allocated stack memory, a variable of reference type allocates heap memory. However, there are a lot of places to allocate heap memory that will surprise us, because there is no need to allocate heap memory. Let's look at some common unnecessary heap memory allocations and come up with the best way to reduce the situation.
String
In C #, string is a reference type and not a value type, although it looks like a value that holds a string. This means that when a string is created and discarded, it generates memory garbage. As a type commonly used in a large number of code, these memory garbage will grow.
A string type colleague in C # is also an immutable type, meaning that it cannot change its value after it is created. When we manipulate one string at a time (for example, using the overloaded operator + to concatenate two strings). Unity if you need to update the value of a string, it will create a new string to hold the new value and discard the current string. This generates memory garbage.
We can follow some simple rules to minimize the memory garbage generated by string. Consider these rules first, and then see how they are applied in the example.
- We should reduce those unnecessary string creation. If we use the same string value more than once, then we should create this string and then cache it.
- We should reduce the change in the value of the string which is not necessary. For example, if we have a text component that is frequently updated, the text component displays a value of two strings each time it is concatenated into a new string. We should consider dividing this text component into two text components, so that the values of two strings are displayed, so that you do not need to create a new string each time.
- If we need to build a string (such as multiple string values concatenated into a string) while the game is running. We should use the StringBuilder class. The StringBuilder class is specifically designed to build a string that does not generate memory allocations and avoids generating large amounts of memory when we connect to a more complex string.
- Once we do not need to debug the project we should remove all the Debug.Log () in the project. Calling Debug.Log () in our game will build the string continuously, even if they are not outputting any debug logs. Every call to Debug.Log () creates and destroys at least one string object, so if we have a large number of these calls in the game, then the memory garbage will increase a lot.
Let's write an example to test that this example contains an inefficient string which results in unnecessary memory garbage generated by the code. In the following code, we create a string object that records the fractional value, which is merged in update () and a float type object with a value of "Time: ". Unnecessary memory garbage is generated here.
public Text timerText;private float timer;void Update(){ timer += Time.deltaTime; timerText.text = "TIME:" + timer.ToString();}
Take a look at the code listed below, and we've made some more effective improvements in some places. We independently assign the "time:" String to a single text component in Start (). This means that we no longer need to fit the string in the update () function. This is very effective in reducing the amount of memory waste generated.
public Text timerHeaderText;public Text timerValueText;private float timer;void Start(){ timerHeaderText.text = "TIME:";}void Update(){ timerValueText.text = timer.toString();}
Unity's function call
It is important to note that when we call code that is not implemented by ourselves, the code includes Unity's own engine code or third-party plug-in code, which can generate memory garbage. Calling some unity functions allocates heap memory, so we should be more careful to avoid unnecessary memory garbage generation.
There is not a list of functions that should be forbidden. Each function can be very useful in some situations, but it is not much use in other situations. Or that sentence, it is best to carefully divide the analysis of our game, find out where the memory garbage created, carefully consider how to deal with it more appropriate. In some cases, the result of the caching function is wiser, but in other cases the wisest way is to reduce the frequency of the function's invocation, and in some cases, the best way is to refactor the code into different functions according to different situations. Old way, let's look at two common functions in unity that cause heap memory allocation. I think about how best to deal with these two functions.
Every function we visit unity returns an array, and this function memory creates a new array to return to us as the return value. This behavior is not always obvious or predictable, especially if the function is a variable (for example, mesh.normals) that is defined as get form access.
The code listed below will create a new array in this iteration loop without looping.
void ExampleFunction(){ for (int i=0; i<meMesh.normals.Length; i++) { Vector3 normal = myMesh.normals[i]; }}
In this example it is easy to take these methods to reduce the memory allocation: we can simply cache a reference to this array. When we want to do things in the code, we just need to create an array, which reduces the amount of memory allocation that can lead to a lot of memory garbage generation.
The following code is a demonstration of this change. In this code, we first call mesh.normals to cache a reference to the array before the loop is run so that only one array is created.
void ExampleFunction(){ Vector3[] meshNormals = myMesh.normals; for (int i=0; i<meshNormals.Length; i++) { Vector3 normal = meshNormals[i]; }}
Another, more unexpected cause of heap memory allocation can be found in gameobject.name or gameobject.tag . These two functions are also get-type-accessed variables that return a new string, which means that calling these functions will result in memory garbage. A more efficient approach is to cache these variable values, but in some cases we can use unity's functions instead of calling these methods that generate memory garbage. To check the tag value of Gameobject without generating the memory garbage, we can use Gameobject.comparetag ().
In the following code, calling Gameobject.tag creates a memory garbage.
private string palyerTag = "Player";void OnTriggerEnter(Collider other){ bool isPlayer = other.gameObject.tag == playerTag;}
If we use Gameobject.comparetag (), this function will no longer generate any memory garbage.
private playerTag = "Player";void OnTriggerEnter(Collider other){ bool isPlayer = other.gameObject.CompareTag(playerTag);}
Gameobject.compatetag is not the only such function of unity; Many unity functions have versions that do not cause memory allocations. For example, we can use Input.gettouch () and input.touchcount instead of input.touches, which can be used Physics.sperecastnonalloc () to replace Physics.spherecastall ()
Packing
Boxing (Boxing) occurs where a value type variable is used as a reference type variable. It usually occurs when we pass a variable of a value type to a function whose argument is a reference type. For example, the value passed to Object.Equals () is an int or float value, but the parameter type of Object.Equals () is a reference type.
For example, in function Stirng.format () the string and object parameters are used, and when we pass in a string and an int parameter, the variable of type int is boxed. The following code contains an example of boxing:
void ExampleFunction(){ int cost = 5; string displayString = String.Format("Price:{0}gold",cost);}
Boxing internally creates a memory garbage. When a variable of a value type is boxed, unity creates a zero-System.Object object package on the heap that wraps the variable of the value type. System.Object refers to a variable of type, so when a variable of this zero is destroyed, a memory garbage is generated.
Boxing is a very common cause of unnecessary heap memory allocations. Even if we are in our own code to avoid the boxing of this variable, but we may use a third-party plug-in will cause boxing or plug-in code inside the implementation of the existence of boxing. The best practice is to avoid any possible boxed code, while removing all the functions that lead to boxing.
Co-process
Calling Startcoroutine () creates a small amount of memory garbage, because unity must create an instance to manage the Coroutine instance returned by Startcoroutine () . With this in mind, minimize the call to Startconoroutine , because the interaction and performance of our games is a core. In order to reduce the memory garbage caused by the process, any process that runs at a time of high performance load should be executed in advance and we should pay special attention to those nested threads, which may also contain deferred call Startcoroutine.
Yield life within the process does not create heap memory on its own. However, the value of the yield state we pass may result in unnecessary heap memory allocations. For example, the following code produces memory garbage:
yield return 0;
This code is boxed (Boxing) because the value 0 will cause memory garbage. In this example, if we want to not generate any heap memory implementations simply wait for a frame effect, the best way is to do this:
yiled return null;
Another common mistake is to create a yield state variable with the same value multiple times using the new operation within the coprocessor. For example, the following code creates and destroys a Waitforseconds object each time it loops.
while(!isComplete){ yield return new WaitForSeconds(1f);}
If we cache waitforseconds this variable and then reuse it, this back greatly reduces the memory garbage. The following code shows how to modify it:
WaitForSeconds delay = new WaitForSeconds(1f);while (!isComplete){ yield return delay;}
If our code generates a lot of memory garbage during the process, we might want to consider refactoring this part of the code using a non-coprocessor approach. Refactoring code is a complex topic and each project is unique, but there are also two alternative ways to use the common process that we want to remember. For example, if we use the co-process primarily to manage time, we can keep a simple time record in the Update () function. If we use the coprocessor primarily to control some of the logical business execution order in our game, we can create some sort of ordered message sending system to allow communication between objects. There are no methods that can fit all situations, but keep in mind that there are often more than one way to achieve the same purpose in your code.
foreach Loop
The specific analysis of this problem can be seen in C # foreach caused by memory problems this article
In previous versions of Unity5.5, a foreach traversed any collection outside the array, generating memory garbage after each loop. This is due to a boxing operation occurring inside the foreach. When a loop is started, a System.Object heap memory object is created, and when the loop ends, the object is destroyed, resulting in a memory garbage. This issue was fixed in the Unity5.5 version.
For example, in a previous version of Unity5.5, the following loop would generate memory garbage:
void ExampleFunction(List listOfInts){ foreach(int currentInt in listOfInts) { DoSomething(currentInt); }}
If we are not able to upgrade our unity version, here's another simple workaround. the for and while loops do not internally cause a boxing operation and therefore do not produce heap memory garbage. When we are going to traverse a collection that is not an array, we should be more inclined to use both methods.
The following code loops through the loop without causing the memory garbage to occur:
void ExampleFunction(List listOfInts){ for (int i=0; i<listOfInts.Count; ++i) { int currentInt = listOfInts[i]; DoSomething(currentInt); }}
References to functions
Whether it is an anonymous function or a normal function, it is a variable of reference type in unity. They all produce heap memory allocations. Converting an anonymous method to a closure (an anonymous function that accesses external variables internally) can add a lot of memory overhead and create multiple heap memory objects at the same time.
More precise and detailed function references and closure memory allocation changes depend on the settings of the platform and compiler on which they reside. However, if GC memory recycling is a relatively important issue, it is best to reduce function references and closure usage during the game run. [This Unity best Practice Guide on performance] has more in-depth technical details about this topic.
LINQ and Regular Expressions
Both LINQ and regular expressions internally generate memory garbage because of boxing. It is a best practice to prohibit the use of them at a centralized point in performance consumption. Or this article, this Unity best Practice Guide on performance provides very good technical details about this topic
Refactoring our code to minimize the impact of GC memory recycling
The way our code is architected affects the GC's memory recycling. Even if our code does not create any heap memory, it will also increase the burden on the GC.
One of the things that our code doesn't need to increase the burden of the GC is that it needs to be checked (to see if the object is referencing the case to decide whether to reclaim the object). A struct is a variable of a value type, but when we have a struct that contains a variable of a reference type, the GC must check the entire struct. If we had a large array of these structures, that would create a lot of extra work for the GC.
In this example, the struct contains a string type object, which is a reference type. When this code runs, the GC must check the entire struct array.
public strct ItemData{ public string name; public int cost; public Vector3 position;}
private ItemData[] itemData;
In the following example, we store the data in a separate array. When the GC is running, it only needs to check arrays of type string and ignore arrays that call other value types. This allows the GC to do only the necessary work.
private string[] itemNames;private int[] itemCosts;private Vector3[] itemPositions;
Another thing that adds an unnecessary burden to GC in our code is that there is only an unnecessary memory reference. When the GC looks for a reference to an object on the heap memory, it must check the current reference for each object in our code, even if we do not need to reduce the total amount of objects on the heap memory, and fewer object references in the code mean that the GC needs to do less work.
In the following example, we implement a dialog box class. When the user views the dialog, a dialog box is displayed. Our code contains a reference to the next instance of the dialog data that should be displayed. This means that the GC must check for references to this part of the operation:
public class DialogData{ private DialogData nextDialog; public DialogData GetNextDialog() { return nextDialog; }}
Here we refactor the code so that the function returns a pointer to an identifier like a dialog data instance instead of returning the instance itself. There is no reference after the modification, which does not increase the GC's time overhead.
public class DialogData{ private int nextDialogID; public int GetNextDialogID() { return nextDialogID; }}
This is a very simple example. However, if our game contains a lot of objects that hold references to other objects. Then we need to consider using this method to refactor our code and reduce the complexity of the heap memory objects.
Timed GC Memory reclamation manually forcing a memory recycle
Eventually, we might want to trigger the GC to reclaim the memory ourselves. If we know that the heap memory is already allocated but is no longer used (for example, if our code generates a memory garbage while loading the resource), we know that the GC recycle memory operation does not affect the player (for example, when the loading interface has been displayed), We can use the following code to require the GC to perform a memory reclamation operation.
System.GC.Collect();
This code forces the GC to run, allowing us to release memory at this point in time for objects without any references.
Summarize
We learned how Unity's GC memory recycling works, and what causes performance problems? How to reduce the impact of these problems on our game. Using the knowledge described in this article and our performance optimization tools, we can fix the performance problems of GC memory recycling and refactor our code to make them manage memory more efficiently.
Unity Game Memory Recovery Optimization (translation)