Memory Optimization for Collections in Unity

Memory Optimization for Collections in Unity

Why Does It Matter?

For memory management, Unity (IL2CPP) uses the conservative Boehm–Demers–Weiser (BDW) garbage collector, which suspends the program's process and resumes normal execution only after garbage collection is over. If your game has many memory allocations, sometimes you will experience short freezes when the garbage collector frees up memory. Starting with Unity 2019, garbage collection works in incremental mode by default, which is better, but not so good that you can forget about memory allocation.

The following optimizations for collections are easy to follow and do not require extra effort to use, but they help avoid unnecessary memory allocations.

Capacity Parameter

The popular collection List<T> has a constructor that allows you to pass capacity as a parameter. Always try to pass the capacity parameter when you create a new List<T>. This reduces unnecessary garbage generation and heap segmentation. As a result, there is less memory consumption for free.

Under the hood, List<T> stores data in an internal array. If you try to add more items to the List<T> than the array can contain, List<T> creates a new array, double the size (in the new .NET version it can be triple) of the previous one and copies the data into the new array. When you pass the 'capacity' parameter in the constructor, it sets the initial size of the array. If you estimate the maximum size of the list accurately, no additional memory allocation will be performed. This is our goal. If you do not pass 'capacity' in the constructor, the internal array allocation size will start at 0, then 4, and will double each time. So, the sequence will be the following: 0-4-8-16-32... Let's say you insert 50 items in such a list - 4 arrays will be generated as garbage.

Here is an example:

This is a good example of list creation. Despite knowing that not all rawDescriptors will be converted to list items, we use 'rawDescriptors.Length' as the List capacity because we can be sure it is the maximum possible item count. It is much better to slightly exceed the capacity than to not meet our capacity expectation.

Here is another example:

This is another good example. Here, we cannot determine the capacity from the class code. However, using some knowledge about the codebase, we can make guesses about how many items are usually stored in '_historyItems' and set up the capacity based on these guesses. As an alternative, we can run the code and see in debug mode how many items are usually stored.

Collection Buffers

Sometimes, we need to design APIs that return a subset of an existing collection. Here's an example of such a class and its usage:

In this example, each time the 'GetItems()' method is called, a new collection containing the subset is created. For such common usage scenarios it is fine.

However lets imagen scenario when code will call GetItems() many times.

The code in this example can become problematic. Since we call 'GetItems' numerous times, and the number of calls is undefined, this code will trigger an indefinite number of allocations, which depend on external factors (user input or the logic of external services), making it unreliable.

To address this, we need to redesign the API using a buffer for collections.

In this example, the code that forms the subset does not create allocations, and the code that calls the API can control the number of allocations through the buffer. Regardless of the number of calls, there will be only one allocation (in the worst case, there will be a reallocation if the result does not fit in the buffer, but this is much better than allocating each time).

Frequent Misconceptions About List<T>

There is no multiple allocations happen when you pass initial list elements to the initializer, without passing the initial size parameter. For example: (new List<int> { 1, 2, 100500})

Unfortunately it does allocations. Hope new .net versions will bring new compiler optimizations, but for now List<int> {1, 2, 42..} unfolds to:

There is some 'initial' capacity in the List<T> by default.

No. We can inspect of List<T> implementation of default List constructor.

So, no additional allocations for empty lists.

Summary

These minor optimizations do not require significant development effort, but they contribute to the smooth running of the game/application. I personally see no reason not to use them.

要查看或添加评论,请登录

Dzmitry Bazyleu的更多文章

社区洞察

其他会员也浏览了