Work optimally with C# data structures and Unity APIsLast updated: December 2018
What you will get from this page: solutions for working optimally with Unity APIs and data structures. Challenges covered include recursive iteration with Particle System APIs; frequent memory allocation; APIs that return Arrays; choosing incorrect data structures, and too much overhead from the misuse of dictionaries.
A lot of these tips will introduce additional complexity, which results in higher maintenance overhead and the possibility for more bugs. Therefore, profile your code first before applying some of these solutions.
All of these tips are from the Unite talk, Squeezing Unity: Tips for raising performance. For updated tips from Ian that take into account the evolution of Unity’s architecture to data-oriented design, see Evolving best practices.
Problem: recursive iteration with Particle system APIs:
When you call one of the Particle System’s main APIs, such as Start, Stop, IsAlive(), it iterates recursively, by default, through all the children in a particle system’s hierarchy:
- The API finds all the children in a particle system’s Transform, calls GetComponent on every Transform, and if there is a Particle System, invokes the appropriate internal method.
- If those child Transforms have their own children, it will recurse all the way down to the bottom of the Transform’s hierarchy–every child, grandchild, and so on, will be processed. This can be a problem if you have a deep Transform hierarchy.
These APIs have a withChildren parameter that defaults to true. Set it to false to eliminate this recursive behavior. When withChildren is set to false, only the particle system that you are calling directly will be changed.
Problem: Multiple particle systems that start and stop at the same time
It’s common for artists to create visual effects that have particle systems spread across a number of Child Transforms, all of which you might want to start and stop at the same time.
Create a MonoBehaviour that caches the list of Particle Systems by calling list GetComponent at initialization time. Then, when the Particle Systems need to be changed, call Start, Stop, etc., on each of them in turn, and make sure to pass false as the withChildren parameter.
Problem: Frequent memory allocation
When a C# closure closes over a local variable, the C# runtime has to allocate a reference on the heap to keep track of the variable. In Unity versions 5.4 through 2017.1, there are a couple of Particle System APIs that, when called, use closures internally. In these Unity versions, all calls to Stop and Simulate will allocate memory, even if the particle system has already been stopped.
All Particle System APIs, like most of Unity’s public APIs, are just C# wrapper functions around internal functions that carry out the actual work of the API. Some of these internal functions are pure C#, but most end up calling down into the C++ core of the Unity engine. In the case of the Particle System APIs, all the internal APIs are conveniently named “Internal_” followed by the name of the public function, such as:
...and so on.
Instead of letting Unity’s public APIs call its internal helpers via closures, you can write an extension method, or address the internal method via Reflection and cache the reference for later use. Here are the relevant function signatures:
All of the arguments have the same meanings as the public API, except the first, which is a reference to the Particle System that you want to stop or to simulate.
Problem: APIs that return Arrays
Any time you access a Unity API, that returns an array, it’s going to allocate a fresh copy of that array. This occurs both when using functions that return arrays and when using properties that return arrays. Common examples of this are Mesh.vertices: access this and you’ll get a fresh copy of the mesh’s vertices. And Input.touches, which will give you copy of all the touches the user is performing during the current frame.
Many Unity APIs now have non-allocating versions, and you should use these instead. For example, instead of Input.touches, use Input.GetTouch and Input.touchCount. From version 5.3 and forward, non-allocating versions of all Physics query APIs have been introduced. For 2D applications, there are also non-allocating versions of all Physics2D query APIs. Read more about the non-allocating versions of Unity’s Physics APIs here.
Finally, if you use GetComponents or GetComponentsInChildren, there are now versions that accept a generic, templated list. These APIs will fill up the list with the results of the GetComponents call. This is not purely non-allocating: if the results of the GetComponents call exceeds the capacity of the list, the list will be resized. But, if you’re reusing or pooling the list, then at least the frequency of the allocations will drop over the course of your application’s lifetime.
Optimal use of data structures
Problem: Choosing incorrect data structures
Avoid choosing data structures that are only convenient to use; instead choose the one that has the performance characteristics that align best with the algorithm or game system that you are writing.
Indexing into an array or a list is extremely cheap: it requires on a little bit of addition under the hood and occurs in constant time. Therefore, randomly indexing into, or iterating through, an array or a List has extremely low overhead. So, if you’re iterating through a list of things every frame, prefer arrays or Lists.
If you need to have constant time addition or removal, you will likely want to use a Dictionary or Hashset (more on this below).
If you’re relating data in a key-value manner, wherein a piece of data is related to another in a one-way manner, then use a Dictionary.
More on Hashsets & Dictionaries: Both of these data structures are backed by a hash table. Remember that a hash table has a number of buckets; each bucket is basically a list that holds all of the values which have a specific hash code. In C#, this hash code comes from the GetHashCode method. Since the number of values in a given bucket is usually much smaller than the total size of the Hashset or Dictionary, adding and removing things from a bucket is much closer to constant time than adding or removing things randomly from a List or array. The precise difference depends on the capacity of your hash table and the number of items you’re storing in it.
Checking for the presence (or absence) of a given value in a Hashset or Dictionary is very cheap for the same reason: it’s quick to check over the (relatively small) number of values in the bucket that represents the value’s hash code.
Problem: Too much overhead from the misuse of dictionaries
If you want to iterate over pairs of data items every frame, often people use a dictionary because it’s expedient. The problem with this is that you’re iterating over a hash table. That means every single bucket in that hash table has to be iterated over, whether or not it contains any values., This adds considerable overhead, especially when iterating over hash tables with few values stored inside them.
Instead, create a structure or tuple, and then store a List or array of those structures or tuples that contain your data relations. Iterate over this List/array instead of iterating over the Dictionary.
Around 14:25 minutes into Ian’s talk, he has a quick tip about how and when to use InstanceID-keyed dictionaries to reduce overhead.
Problem: What if you have multiple concerns?
In the real world, of course, most problems present you with multiple overlapping requirements and there will not be one data structure that meets all of your needs.
A common example could be an Update Manager. This is an architectural programming pattern, whereby an object (usually a MonoBehaviour) distributes Update callbacks to different systems inside of your game.When systems want to receive Updates, they subscribe to them via the Update Manager object.For such a system, you’d need low-overhead iteration, constant time insertion and, constant-time duplicate checks.
Solution: Use two data structures
- Maintain a list or array for iteration.
- Before you change the list, use a Hashset (or another kind of indexing set) to make sure the item that you’re adding or removing is actually present.
- If removal is a concern, consider a linked list or intrusively-linked list implementation (be aware that this results in higher memory cost and slightly higher iteration overhead).