Unity optimization - script optimization strategy 2

 Hello everyone, this is Qiqi. Today we will continue to introduce several Unity script optimization strategies.

Table of contents

1. Faster GameObject null reference check

2. Avoid taking out string properties from GameObject

 3. Use appropriate data structures

4. Avoid modifying the parent node of Transform during runtime


1. Faster GameObject null reference check

It turns out that performing a null reference check on GameObject causes some unnecessary overhead. GameObject and MonoBehaviour are special objects compared to typical C# objects because they have two representations in memory: one representation lives in the same system-managed memory that manages the C# code that the user wrote (code hosting) , while the other representation exists in another separately processed memory space (native code). Data can be moved between these two memory spaces, but each such move results in additional CPU overhead and possibly additional memory allocations.

This effect is often referred to as spanning native-hosted bridging. If this happens, additional memory allocations may be generated for the object's data to bridge the copy, requiring the garbage collector to eventually perform some automatic memory cleanup operations. This article will not go into details. For now, just know that there are many subtle ways to accidentally trigger this extra overhead, and a simple null reference check on a GameObject is one of them:

if(gameObject != null){
//对gameObject做一些事情
}

An alternative is System.Object.ReferenceEquals(), which produces functionally equivalent output and runs about twice as fast (although it does obfuscate the code's purpose a bit).

  if (!System.Object.ReferenceEquals(gameObject, null))
        {
            //对gameObject做一些事情
        }

This applies to both GameObject and MonoBehaviour, but also to other Unity objects, which have both native and managed representations, such as the WWW class. However, some basic testing shows that either null reference checking method still only consumes nanoseconds. So unless you perform a lot of null reference checking, you'll gain little benefit at best. However, this is a warning worth remembering in the future, as it will come up often.

2. Avoid taking out string properties from GameObject

In general, retrieving a string property from an object is the same as retrieving any other reference type property in C#; such retrieval should impose no memory cost. However, retrieving string properties from a GameObject is another subtle way to accidentally cross the native-managed bridge.

The two properties in GameObject affected by this behavior are tag and name. Therefore, it is unwise to use both properties during gameplay, and they should only be used where performance does not matter, such as editor scripts. However, tag systems are often used for runtime identification of objects, which is an important issue for some teams.

For example, the following code causes additional memory allocation on each iteration of the loop:

for(int i= 0; i < listOfObjects.Count; i++)
        {
            if (listOfObjects[i].tag == "player")
            {
                //对这个对象做一些事
            }
        }

It's generally a better practice to identify objects based on their components and class types, and to identify values ​​that don't involve string objects, but sometimes it can get bogged down. Perhaps without knowing it at first, we inherited someone else's code base, or used it as a workaround. Let's say for some reason something goes wrong with the tagging system and we want to avoid the overhead cost of a local-to-hosted bridge.

Fortunately, the tag attribute is most commonly used for comparison, and GameObject provides theCompareTag() method, which is another way to compare tag attributes. An approach that avoids native-hosted bridging entirely.

Use the CompareTag() method to replace the direct comparison method above, and draw the conclusion through profiler analysis: the processing time is reduced by half, and since it does not cause memory allocation, it will not cause garbage collection.

This means that accessing the name and tag attributes must be avoided whenever possible. If comparison of tags is required, CompareTag() should be used. However, the name attribute has no corresponding method, so use the tag attribute whenever possible.

Tip: Passing strings to CompareTag() does not cause runtime memory allocation, so applications allocate such hardcoded strings during initialization and just reference them at runtime.

 3. Use appropriate data structures

There are many different data structures available in the C#System.Collections namespace and we should not use the same namespace over and over again. A common performance problem in software development is using inappropriate data structures to solve a problem simply for the sake of convenience. The two most common data structures are List<T> and Dictionary<K,V>.

If you want to iterate over an object, it's better to use a list, because it is actually a dynamic array, and the objects and references are adjacent to each other in memory, so the cache miss caused by iteration is minimal. If two objects are related to each other and you want to quickly retrieve, insert, or delete these relationships, it's best to use a dictionary. For example, you could associate a level number with a specific scene file, or associate an enum representing different body parts of a character with the Collider components for those body parts.

However, data structures often need to handle two things at the same time: quickly finding out which object maps to another object, while also being able to iterate over the group. Typically, developers of this system use dictionaries and then iterate over them. However, this process is very slow compared to traversing a list because it must check every possible hash in the dictionary before it can fully traverse it.

In these cases, it is better to store the data in lists and dictionaries to better support this behavior. This requires additional memory overhead to maintain multiple data structures, and insertion and deletion operations require adding and removing objects from the data structure each time, but the benefits of iterating over lists are in stark contrast to iterating over dictionaries.

4. Avoid modifying the parent node of Transform during runtime

In earlier versions of Unity, references to Transform components were often randomly ordered in memory. This means that iterating over multiple Transforms is quite slow because of the possibility of cache misses. The advantage of this is that modifying the parent node of GameObject to another object will not cause significant performance degradation, because Transform operates like a stack of data structures, and insertion and deletion are relatively fast. This behavior is beyond our control, so we have to accept it.

However, after Unity5.4, the memory distribution of the Transform component has changed a lot. From that point on, the parent-child relationship of Transform components operates more like a dynamic array, so Unity attempts to store all Transforms sharing the same element sequentially in memory in a pre-allocated memory buffer and display them in the Hierarchy window based on the parent element. Sort by depth. This data structure allows for faster iteration across the entire group, which is particularly beneficial for multiple subsystems such as physics and animation. The disadvantage of this change is that if you reparent a GameObject to an Object, the parent must put the new children into a pre-allocated memory buffer and order those Transforms according to the new depth. Additionally, If the parent object does not pre-allocate enough space to accommodate the new child object, the buffer must be expanded to accommodate the new child object and all of its children in depth-first order. For deeper, complex GameObject structures, this may take some time to complete.

When instantiating a new GameObject through GameObject.Instantiate(), one of its parameters is the Transform that you want to set the GameObject to as its parent node. The default value is null. Place the Transform under the root element of the Hierarchy window. All Transforms under the root element of the Hierarchy window need to allocate a buffer to store its current child elements and any child elements that may be added in the future (child Transform elements do not need to do this). However, if you re-parent the Transform to another element immediately after instantiation, it will discard the buffer it just allocated! To avoid this, the parent Transform parameter should be provided to the GameObject.Instantiate() call, which skips this buffer allocation step.

Another way to reduce the cost of this process is to have the root Transform pre-allocate a larger buffer before it is needed, so that it does not need to extend the buffer and redirect it to another GameObject in the same frame. This can be achieved by modifying the Transform's HierarchyCapacity property. If you can estimate the number of child Transforms contained in the parent element, you can save a lot of unnecessary memory allocation.

Guess you like

Origin blog.csdn.net/m0_63024355/article/details/134607335