Recently a little busy, day work at night to write some translation, but also to exercise, so the translation work is a bit slow =. =
This article continues with the previous Unity rendering optimization, further translating the GC optimizations in unity, in English with the following: English address
Introduced:
When the game is running, the data is mainly stored in memory, when the game's data is not needed, the memory of the current data can be reused again. Memory garbage refers to the memory consumed by the current discarded data, and garbage collection (GC) refers to the process of recycling obsolete memory back into use.
Garbage collection is considered a part of memory management in unity, and if garbage collection in the game is complex, the performance of the game can be greatly affected, and garbage collection becomes a major obstacle to game performance.
In this paper, we mainly study the mechanism of garbage collection, how garbage collection is triggered and how to improve the efficiency of garbage collection to reduce its impact on game performance.
A brief introduction to unity memory management mechanism
To understand how garbage collection works and when it is triggered, we first need to understand unity's memory management mechanism. Unity uses the mechanism of automatic memory management, which in the code does not need to tell unity how to manage memory in detail, and unity internal memory management.
Unity's automatic memory management can be understood in the following sections:
1) There are two memory management pools inside Unity: heap memory and Stack memory. Stack memory (stacks) is primarily used to store small and ephemeral pieces of data, and heap memory is primarily used to store larger and longer-stored pieces of data.
2) Variables in unity will only allocate memory on the stack or heap memory.
3) As long as the variable is active, the memory it occupies is marked for use, and the memory of that part is in the allocated state, and the variable is either stored on the stack memory or on heap memory.
4) Once the variable is no longer active, the memory it consumes is no longer needed, and the portion of memory can be recycled into the memory pool for reuse, which is a memory recycle. Memory reclamation on the stack and its fast, memory on the heap is not reclaimed in a timely manner, and its corresponding memory is still marked for use.
5) garbage collection mainly refers to the memory allocation and recycling on the heap, and in unity, the heap memory is timed to perform GC operations.
After understanding the GC process, here is a detailed understanding of the differences between the allocation and recycling mechanisms for heap memory and stack memory.
Stack memory allocation and recycling mechanism
Memory allocation and recycling on the stack is fast and easy, mainly because only short, small variables are stored on the stack. Memory allocation and recycling occurs in a controlled order and size.
Stacks run like a stack: it's just a collection of data that runs in a fixed way. It is this simplicity and fixation that makes the stack operation very fast. When the data is stored on the stack, it simply needs to be expanded afterwards. When the data fails, you just need to remove it from the stack and reuse it.
Heap memory allocation and recycling mechanism
Memory allocations and storage on heap memory are more complex, mainly in that the heap memory can store short-term smaller data, or it can store various types and sizes of data. The memory allocation and recycling order on it is not controllable and may require allocating different sizes of memory units to store the data.
The variables on the heap are stored in the following main steps:
1) First, unity detects if there are enough idle memory units to store the data, and if so, allocates the corresponding memory units;
2) If there are not enough storage units, unity triggers a garbage collection to free up heap memory that is no longer in use. This step is a slow operation, and if there are enough memory units after garbage collection, memory allocations are made.
3) If there are not enough memory units after garbage collection, unity expands the heap memory size, which is slow and allocates the corresponding memory units to the variables.
The allocation of heap memory can become very slow, especially if garbage collection and heap memory need to be extended.
What to do when garbage collection
When a variable is no longer active, the memory it consumes is not immediately reclaimed, and memory that is no longer used is recycled only when it is in the GC.
Each time the GC is run, the following are the main things to do:
1) The GC examines each storage variable on the heap memory;
2) For each variable will be detected whether its reference is active;
3) If the reference to the variable is no longer active, it will be marked as recyclable;
4) The tagged variable is removed and the memory it occupies is recycled to the heap memory.
A GC operation is an extremely expensive operation, and the more variables or references on the heap memory, the more operations it takes to run and the longer it takes.
When garbage collection is triggered
There are three main actions that trigger a garbage collection:
1) The memory allocation operation on the heap memory will trigger garbage collection to utilize the unused memory when the memory is not enough;
2) The GC will automatically trigger, different platform operating frequency is not the same;
3) GC can be enforced.
GC operations can be triggered frequently, especially when memory cells are not sufficient when memory is allocated on heap memory, which means that frequent memory allocations and recoveries on heap memory trigger frequent GC operations.
Problems with GC operations
After understanding the role of GC in Unity memory management, we need to consider the problems it poses. The most obvious problem is that GC operations can take a lot of time to run, and if there are a large number of variables on the heap memory or the reference needs to be checked, the check will be slow, which makes the game slow to run. Second, the GC may run at a critical time, such as when the CPU is at a critical moment in the performance of the game, and any additional operations can have a significant impact on the game frame rate.
Another problem with GC is heap memory fragmentation. When a memory unit is allocated from the heap memory, its size depends on the size of the variable it stores. When the memory is recycled into the heap memory, it is possible to split the heap memory into fragmented units. This means that heap memory can be used in a larger memory unit, but a separate memory unit is smaller, the next memory allocation will not be able to find the appropriate size of the storage unit, which triggers a GC operation or heap memory expansion operations.
Heap memory fragmentation can result in two results, one of which is that the game occupies more and more memory, and a GC is triggered more frequently.
Analyze Problems with GC
The problem of GC operation mainly shows that the frame rate is running low, the performance is interrupted or decreased intermittently. If the game is behaving like this, you first need to open the profiler window in unity to determine if the GC is causing it.
To learn how to use the profiler window, you can refer to this, and if the game is actually caused by a GC, you can continue reading the following.
Analyzing the allocation of heap memory
If the GC is causing a performance problem with the game, we need to know what part of the code in the game will cause the GC, and the memory garbage is generated when the variable is no longer active, so first we need to know what variables are allocated on the heap memory.
Variable types for heap memory and stack memory allocations
In unity, value-type variables are memory-allocated on the stack, and other types of variables are allocated on the heap memory. If you don't know the difference between a value type and a reference type, you can check here.
The following code can be used to understand the allocation and deallocation of value types, and the corresponding variables will be recycled immediately after the function call is complete:
void Examplefunciton () { int5; }
The reference code for the corresponding reference type is as follows, and its corresponding variable is recycled in GC:
void examplefunction () { new List (); }
Use profiler window to detect heap memory allocations:
We can check the allocation of heap memory in the Profier window: In the CPU Usage Analysis window, we can detect the memory allocation of any one frame CPU. One of the options is GC alloc, which analyzes it to locate what functions cause a lot of heap memory allocation operations. Once this function is positioned, we can analyze the cause of the problem and reduce the generation of memory garbage.
Ways to reduce the impact of GC
In general, there are three ways to reduce the impact of GC:
1) Reduce the number of GC runs;
2) Reduce the running time of single GC;
3) delay the runtime of the GC to avoid triggering at critical times, such as the ability to invoke a GC when the scene is loaded
Based on this, we can adopt three kinds of strategies:
1) refactoring the game to reduce the allocation of heap memory and the allocation of references. Fewer variables and references can reduce the number of detections in GC operations and increase the efficiency of GC operation.
2) reduce the frequency of heap memory allocations and recoveries, especially at critical times. This means fewer events trigger GC operations and also reduce heap memory fragmentation.
3) We can try to measure the time of GC and heap memory expansion so that it executes in a predictable order. Of course, this is a very difficult operation, but it can greatly reduce the impact of GC.
Reduce the amount of memory waste
There are several ways to reduce the amount of memory waste:
Cache
If some of the functions that cause heap memory allocations are called repeatedly in code, but the results are not used, this creates unnecessary memory garbage, which we can cache to reuse, which is the cache.
For example, the following code will cause heap memory allocations each time it is called, basically allocating a new array each time:
void Ontriggerenter (Collider other) { renderer[] allrenderers = findobjectsoftype<renderer> (); Examplefunction (allrenderers); }
In contrast to the following code, only one array is produced to cache the data for reuse without causing more memory waste:
Private renderer[] Allrenderers;void Start () { allrenderers = findobjectsoftype<renderer> ();} void Ontriggerenter (Collider other) { examplefunction (allrenderers);}
Do not repeat heap memory allocations in frequently called functions
In Monobehaviour, if we need to do heap memory allocations, the worst thing is to do heap memory allocations in the functions they call repeatedly, such as the update () and Lateupdate () functions, which each frame calls, which can cause a lot of memory waste. We can consider allocating memory in the start () or awake () function, which can reduce memory waste.
In the following example, the UPDATE function triggers the generation of memory garbage multiple times:
void Update () { examplegarbagegenerationfunction (transform.position.x);}
With a simple change, we can make sure that the function call is triggered every time the x changes, thus avoiding heap memory allocations per frame:
Private float Previoustransformpositionx; void Update () { float transformpositionx = transform.position.x; if (Transfrompositionx! = Previoustransformpositionx) { examplegarbagegenerationfunction (Transformpositionx); = Trasnformpositionx; }}
Another way to do this is to use timers in the update, especially in code that runs with regularity but does not need to run every frame, for example:
void Update () { examplegarbagegeneratiingfunction ()}
By adding a timer, we can ensure that the function is only triggered once every 1s:
Private float timesincelastcalled;private Float delay = 1f;void Update () { timsincelastcalled + = Time.deltatime; if (timesincelastcalled > Delay) { examplegarbagegenerationfunction (); timesincelastcalled = 0f; }}
With such a small change, we can make the code run faster while reducing the generation of memory garbage.
Clear Linked list
When the list is allocated on the heap memory, if the linked list needs to be allocated multiple times, we can use the clear function of the list to empty the linked list, instead of creating the distribution list repeatedly.
void Update () { list myList = new List (); Populatelist (myList); }
By improving, we can reduce the generation of memory garbage only when the list is first created or the linked list must be reset, which greatly reduces the production of the heap:
Private list myList = new list (); void Update () { mylist.clear (); Populatelist (myList);}
Object Pool
Even if we minimize the allocation of heap memory in our code, it can still cause a GC if the game has a large number of objects that need to be generated and destroyed. Object pooling technology can reduce the allocation and recycling frequency of heap memory by reusing objects. Object pooling is widely used in games, especially when the game requires frequent creation and destruction of the same game objects, such as gun bullets.
A detailed explanation of the object pool is beyond the scope of this article, but this technique deserves a deep dive into the tutorial on object pooling on the Unity learn site for an in-depth explanation of the object pool.
Factors that cause unnecessary heap memory allocations
We already know that value type variables are allocated on the stack, other variables are allocated on the heap memory, but there are still some cases where heap memory allocations can surprise us. Let's analyze some of the common unnecessary heap memory allocation behaviors and optimize them.
String
In C #, a string is a reference-type variable rather than a value-type variable, even if it looks like it is a value that stores a string. This means that the string will cause a certain amount of memory garbage, because the code often uses strings, so we need to be extra careful.
The strings in C # are immutable, meaning that their internal values cannot be changed after they are created. Each time a string is manipulated (for example, by using the "plus" action of a string), Unity creates a new string to store the old string, which causes the memory garbage to be discarded.
We can use some of the following methods to minimize the effect of strings:
1) reduce the creation of unnecessary strings, and if a string is used more than once, we can create and cache the string.
2) Reduce unnecessary string operations, for example, if some strings in the text component need to be changed frequently, but others do not, then we can divide it into two parts of the component.
3) If we need to create strings in real time, we can use Stringbuilderclass instead, StringBuilder is designed for no memory allocations, thus reducing the amount of memory garbage generated by strings.
4) Remove the code for the Debug.Log () function in the game, although the function may output null, the call to the function will still execute, and the function will create a string of at least one character (the null character). If there is a large number of calls to this function in the game, this will cause the memory garbage to increase.
In the following code, a string operation is performed in the update function, which creates unnecessary memory garbage:
Public Text timertext;private float timer;void Update () { timer + = Time.deltatime; Timertext.text = "Time:" + timer. ToString ();}
By separating the strings, we can eliminate the added manipulation of the strings, thus reducing unnecessary memory garbage:
Public text timerheadertext;public text timervaluetext;private float timer;void Start () { Timerheadertext.text = " Time: ";} void Update () { Timervaluetext.text = timer. ToString ();}
Unity function call
In code programming, we need to know that when we call code that is not written by ourselves, either unity or plug-ins, we can generate memory garbage. Some of Unity's function calls generate memory garbage, and we need to be aware of its use when we use it.
There is no clear list of what functions need to be noted, and each function is used differently under different circumstances, so it is best to carefully analyze the game, locate the cause of the memory garbage, and how to solve the problem. Sometimes caching is an effective way to minimize the frequency of calls to a function, and sometimes it is a way to refactor code with other functions. Now analyze the common function calls in unity that cause heap memory allocations.
In unity, if the function needs to return an array, a new array is allocated for the result return, which is not easy to notice, especially if the function contains iterators, and the following code produces a new array for each iterator:
void examplefunction () { for (int i=0; i <mymesh.normals.length;i++) { = mymesh.normals[i];} }
For such a problem, we can cache a reference to an array, so that only one array can be allocated to achieve the same function, thus reducing the generation of memory garbage:
void Examplefunction () { vector3[] meshnormals = mymesh.normals; for (int i=0; i < meshnormals.length;i++) { Vector3 normal = Meshnormals[i];} }
In addition, a function call gameobject.name or Gameobject.tag can also cause unexpected heap memory allocations, both of which will save the result as a new string return, which will result in unnecessary memory garbage, which is an effective way to cache the results, but in unity there are corresponding functions to replace. For the comparison of gameobject tag, you can use Gameobject.comparetag () to replace.
In the following code, calling Gameobject.tag generates memory garbage:
private string playertag= "Player"; void Ontriggerenter (Collider other) { bool Isplayer = Other.gameObject.tag = = Playertag;}
Using Gameobject.comparetag () can avoid the generation of memory garbage:
private string Playertag = "Player"; void Ontriggerenter (Collider other) { bool Isplayer = Other.gameObject.CompareTag (Playertag);}
Not only many other functions in gameobject.comparetag,unity can also avoid the generation of memory garbage. For example, we can use Input.gettouch () and Input.touchcount () instead of input.touches, or with Physics.spherecastnonalloc () To replace Physics.spherecastall ().
Boxing operations
A boxing operation is an internal transformation process where a value type variable is used as a reference type variable, which triggers a boxing operation if we pass in a value type to a function with an object type parameter. For example, the String.Format () function needs to pass in string and object type parameters, and if the string and int type data are passed in, the boxing action is triggered. As shown in the following code:
void Examplefunction () { int cost = 5; String displaystring = String.Format ("price:{0} Gold", cost);}
In Unity's boxing operations, for value types, a reference to a System.Object type is allocated on the heap memory to encapsulate the value type variable, and its corresponding cache generates memory garbage. Boxing is a very common behavior of generating memory garbage, even if the code does not directly boxing the variable, in the plug-in or other functions may be generated. The best solution is to avoid or remove the code that causes the boxing operation as much as possible.
Co-process
Calling Startcoroutine () produces a small amount of memory garbage because unity generates entities to manage the process. So the call to the function should be limited at the critical moment of the game. Based on this, any association that is invoked at the game's critical moment requires special attention, especially the association that contains the delay callback.
Yield does not generate heap memory allocations in the process, but if yield has parameters returned, it can cause unnecessary memory garbage, for example:
Yield return 0;
Due to the need to return 0, a boxing operation is raised, resulting in memory garbage. In this case, in order to avoid the memory garbage, we can return this way:
yield return null;
Another wrong use of the association is that each time it returns, the same variable is new, for example:
while (!iscomplete) { yield return new waitforseconds (1f);}
We can use caching to avoid this kind of memory garbage generation:
Waitforseconds delay = new Waiforseconds (1f); while (!iscomplete) { yield return delay;}
If the in-game process produces memory garbage, we can consider other ways to replace the association. Refactoring the code is very complex for the game, but we can also take note of common actions for the process, such as using a co-process to manage time, preferably keeping a record of the time in the update function. If you use a co-process to control the order in which events occur in the game, it is best to have some way of communicating information between different events. There is no way for the process to be suitable for every situation, only the best solution is chosen based on the specific code.
foreach Loop
In previous versions of unity5.5, memory garbage was generated in the iteration of foreach, mainly from subsequent boxing operations. Each time a foreach iteration is made, a System.Object is produced on the heap memory to enable iterative looping operations. This problem was addressed in unity5.5, for example, in a previous version of unity5.5, using foreach to implement loops:
void Examplefunction (List listofints) { foreach (int currentint in listofints) { dosomething (currentint);} }
If the game project cannot be upgraded to more than 5.5, you can use a for or while loop to solve the problem, so you can instead:
void Examplefunction (List listofints) {for (int i=0; i < listofints.count; i++) { int currentint = Listofints[i]; DoSomething (Currentint); }}
Function reference
A reference to a function, whether it is a pointer to an anonymous function or an explicit function, is a reference type variable in unity, which is allocated on the heap memory. The use of memory and the allocation of heap memory are increased when the invocation of an anonymous function is complete. The reference and termination of a specific function depends on the operating platform and compiler settings, but it is best to reduce the reference to the function if you want to reduce the GC.
LINQ and Constant expressions
Because LINQ and constant expressions are implemented in a boxed manner, it is best to perform performance testing when used.
Refactoring code to reduce the impact of GC
Even if we reduce the allocation of code on heap memory, the code will increase the workload of the GC. The most common way to increase the GC workload is to have it check for objects that it does not have to check. A struct is a variable of a value type, but if a struct contains a variable of a reference type, then the GC must detect the entire struct. If this is a lot of operations, then the amount of GC work is greatly increased. In the following example, a struct contains a string, and the entire struct must be checked in the GC:
public struct itemdata{public string name; Public int. cost; public Vector3 position;} Private itemdata[] ItemData;
We can reduce the amount of GC work by splitting the struct into multiple arrays:
Private string[] Itemnames;private int[] itemcosts;private vector3[] itempositions;
Another way to increase the GC workload in your code is to save an unnecessary object reference, which checks the object reference on the heap memory while the GC operation is in progress, and the fewer references mean less effort to check. In the following example, the current dialog box contains a reference to the next dialog box, which allows the GC to go back to check the next object frame:
public class dialogdata{ private Dialogdata nextdialog; Public Dialogdata Getnextdialog () { return nextdialog; }}
By refactoring the code, we can return the markup for the next dialog entity, not the dialog entity itself, so there is no extra object reference, which reduces the amount of GC effort:
public class dialogdata{ private int nextdialogid; public int Getnextdialogid () { return nextdialogid; }}
Of course This example is not important in itself, but if our game contains a large number of objects that contain references to other objects, we can consider refactoring the code to reduce the amount of GC work.
Perform GC operations on a timed basis
Proactively invoke GC operations
If we know that the heap has been allocated and is not used, we want to be able to invoke the GC operation proactively, or when the GC operation does not affect the game experience (for example, when the scene switches), we can actively invoke the GC operation:
System.GC.Collect ()
With active invocation, we can proactively drive GC operations to reclaim heap memory.
Summarize
This article has a certain understanding of the GC in unity and has a certain understanding of the impact of GC on game performance and how to solve it. We can manage the memory of the game more effectively by locating the code that causes the GC problem and the code refactoring.
Then I will continue to write some unity-related articles. Translation of the work, in the back there is a chance to continue.
Unity Optimization gc--Rational optimization of Unity's GC