This content originally appeared on DEV Community and was authored by Attilio Carotenuto
Memory is an important, and often overlooked, aspect of game optimisation. A game project that correctly manages memory can be the difference between a smooth experience with a high framerate, and a choppy game with frame drops.
Unity uses 3 layers of memory to run your game. Native Memory, used to run the engine itself, is normally the biggest chunk of your game memory footprint. It includes memory from the different native subsystems, such as rendering, Physics, and UI, as well as memory used for assets, scenes, and plugin buffers.
Managed Memory, sometimes also referred to as scripting memory, is what you mainly use when writing C# game code. It comprises three parts, the Managed Heap, the Scripting Stack, and Native VM memory, and it offers a Garbage Collector to automatically allocate and release memory.
Finally, the C# Unmanaged Memory layer is used as a bridge between the Native and Managed layers, allowing you to access and manipulate native memory while writing C# code. It’s typically accessed through the Unity.Collections data structures and UnsafeUtility malloc and free, and while using Burst and the Job system. Memory in this layer doesn’t use a Garbage Collector, so you’ll need to manage memory explicitly, hence the name.
Let’s take some time to understand what tools the Unity Editor offers to track, investigate, and improve memory usage.
Using the Unity Memory Profiler
The Unity Memory Profiler is an external package that allows you to take a snapshot of your game, as it runs, and then analyse memory usage in detail, including what assets are loaded in memory, and a breakdown of all tracked memory by type.
After you take a capture, you’ll see memory divided in various categories. Let’s explain some of those terms.
From the dropdown on the top you can switch between viewing Allocated memory, Resident memory, or both. Resident memory refers to memory used by your application that actually resides on the physical memory hardware (as opposed to virtual memory). When this increases, the game will frequently experience page faults, leading to performance issues. In addition, many OS will track Resident memory usage and potentially terminate your game when it gets too high and the system needs to free up resources.
Reserved memory is memory that is preallocated by the Unity allocators, and generally free to be used for later allocations. This is because requesting memory from the system is relatively slow, so Unity requests large chunks in advance and keeps them available for your game.
When Native memory is allocated, Unity assigns a root as its owner so it can be tracked in the Memory Profiler and in other places. In some cases, the root might be missing, or deleted without clearing up the related children. In this case, the Memory Profiler will track that in the Unknown category.
This is different from Untracked memory, which is memory the game is using, as reported by the operating system, but without a clear indication of its source. This typically comes from native plugins, DLLs, thread stacks, or type metadata.
While you can use the Memory Profiler in the editor Play Mode, this is not recommended as the results are not representative of your game running on device. This is because a capture might include stuff used by the editor itself, the editor might keep stuff in memory even though your game no longer needs them, unload them at different times, and assets will be in a different format than the ones they would have on device (resulting in a different, normally larger, footprint).
You can learn more about the Memory Profiler and all of its features in the official docs.
Configuring Native Memory Allocators
Because requesting memory from the system is relatively slow, Unity uses a variety of allocators to reserve memory based on multiple factors, including allocation persistence, lifespan, size and so on.
Unity will use the Main Thread allocator if the code is running on the main thread. If not, it will use a Thread Allocator. Then, depending on size, it will attempt to use the Bucket Allocator, if it can find a suitable block. If not, it will request it from the Dynamic Heap Allocator. The Dynamic Heap Allocator is slower, and frequently falling back to it can lead to fragmentation. You can find specific technical details about each allocator, along with examples, in this page.
Keep in mind that the way memory is allocated, used, and freed varies depending on what platform and architecture you’re developing on.
Since Unity 2021 LTS, Unity allows you to manually configure Memory parameters to tweak your Native Memory Allocators. Allocators can be configured directly from within the editor, in Project Settings > Memory Settings. When you first enter the window, all values will be locked. You can click on the lock icon to make it writable.
There are separate settings for the Editor and for Player builds. Whenever you tweak allocator settings for the Editor, you’ll need to restart it to ensure the changes are applied. Player changes will be applied on the next build, and written directly into the boot.config file.
Alternatively, you can set allocator sizes using command line arguments, either for the Editor or a game build, specifying the desired size in bytes, for example:
-memorysetup-job-temp-allocator-block-size=2097152
Each allocator has a respective argument, you can find the full list, along with default values, on this page.
In some cases, increasing the size of the Bucket Allocator will lead to small allocations being grouped there, improving performance and reducing fragmentation. On the other hand, this might lead to more memory being reserved for the buckets that stay unused, if your game doesn’t rely as much on small allocations. It’s not possible to give a general rule, as it depends on a lot of different factors for your specific project.
This is an advanced optimization. If you don’t know exactly what you are doing, it’s easy to negatively affect your game performance and introduce issues. For this reason, it should be kept as a last resort, and the changes should always be validated through extensive profiling, on every platform you are supporting.
Configuring the Garbage Collector
As mentioned previously, Unity handles memory in the Managed layer using a Garbage Collector. While this is convenient, it comes with performance overhead, particularly if a game frequently allocates memory that is meant to be used for a short period. This typically shows up in the Profiler as CPU spikes, and results in choppy gameplay experience with periodic stuttering.
By default Unity runs garbage collection incrementally, which means the process is run over multiple frames, resulting in shorter interruptions. While this is recommended in most cases, it has some (relatively minor) added overhead, as it needs to add markers, called write barriers, to function calls that change references.
Another downside of the incremental GC is that it can make it harder to spot memory issues, as the cost of the unnecessary allocations and collections will be spread over multiple frames, hiding GC spikes.
You can optionally set the GC to non-incremental mode, by unchecking Project Settings > Player > Configuration > Use Incremental GC.
When that is disabled, Unity will need to stop the main thread to do a full GC sweep, often resulting in larger GC spikes. This is mainly recommended when developing non-real-time applications, or when you’re confident your title does not trigger the GC in critical sections. It’s also useful to use during development, to make GC spikes more visible during memory investigations, as mentioned earlier. In the vast majority of the cases, you want to keep Incremental GC enabled.
You can also disable GC altogether by setting GarbageCollector.GCMode
to GCMode.Disabled
via scripting. In this case, the GC will not run, and calling the related collection methods will have no effect. This can be beneficial for sections where you allocate all required resources in advance and are certain that no further allocations will occur, then run for a while with GC disabled for better performance, and then re-enable it.
Alternatively, you can set the GC to manual mode by setting the GarbageCollector.GCMode
to GCMode.Manual
. In this case, you’ll need to trigger it manually by calling System.GC.Collect
for a full blocking collection, or GarbageCollector.CollectIncremental
for an incremental collection pass.
Using the Debug Allocator
In some cases, your game might crash due to memory corruption. This might be caused by an issue in your game scripts, a bug in the Unity engine, or both. Due to the way Unity works, the crash might be delayed during execution, making it harder to reproduce and causing loss of useful debug data.
To better investigate these issues, you can add the -debugallocator
command line argument when running the game. When that is set, all freed or out of bounds memory accesses will cause a crash immediately. It does that by allocating a non-accessible extra page right after each allocation. The pages are decommitted but not released to the OS when the related memory is freed, again resulting in a crash if that is accessed.
Setting the debugallocator flag will increase general memory usage, and memory allocation will be slower. For this reason, it’s only meant to be used for debugging sessions, and never for actual game testing or public builds.
While this parameter can also be used when running the editor, it’s not recommended as the editor will become very slow.
Using the System Allocator
Passing the -systemallocator
command line argument when running your game will tell Unity to use the malloc, realloc, and free system calls, rather than Unity’s Dynamic Heap Allocator.
This is helpful when using Native profilers, or tools such as Application Verifier, Address Sanitizers, and Windows Performance Analyzer, as it will cause individual allocations to correctly show during profiling, rather than being lumped together.
This should be used only for debugging purposes. Requesting and releasing system memory is relatively slow, while the Dynamic Heap Allocator allocates memory in pages and then holds them.
Inspecting Memory Statistics
While working in the editor or running a player build, Unity will keep track of Memory and Performance statistics. When closing it, you can have these statistics printed in the related logs.
Here is an example of the first few lines of the Memory section:
Checking for leaked weakptr:
Found no leaked weakptrs.
Memory Statistics:
[ALLOC_TEMP_TLS] TLS Allocator
StackAllocators :
[ALLOC_TEMP_CurlRequest]
Initial Block Size 64.0 KB
Current Block Size 64.0 KB
Peak Allocated Bytes 0 B
Overflow Count 0
[ALLOC_TEMP_MAIN]
Peak usage frame count: [1.0 MB-2.0 MB]: 1 frames, [8.0 MB-16.0 MB]: 2 frames
Initial Block Size 16.0 MB
Current Block Size 19.5 MB
Peak Allocated Bytes 10.7 MB
Overflow Count 0
To enable Memory and Performance statistics in Unity 6 or newer, you’ll need to pass the -log-memory-performance-stats
command line argument when launching the editor or the game.
In older versions of Unity, statistics are enabled by default and cannot be turned off.
Advanced Tools for Memory Analysis
In addition to Unity’s offering for memory profiling and management, there are other useful tools you can rely on to investigate and fix memory problems.
Your first choice here should be the Native profiler for your platform you are working on, as they provide more low-level information and metrics about the underlying hardware running your game. If you are working on Android you can use Android Studio, while on iOS and MacOS you can rely on XCode Instruments. If you are developing a Windows game, Pix is a common choice, while for Arm based games, including Mac with Apple Silicon chipsets, you can use Arm Development Studio.
The Application Verifier, or AppVerifier, is a runtime verification tool for unmanaged code from Microsoft that lets you debug memory corruptions, access violation issues, and more. You can read about it here.
The Address Sanitizer is a memory error detector from Google. It helps you track down memory leaks, Use after free/return/scope, buffer overflows, and initialization order issues. You can read about it here.
Windows Performance Recorder (WPR) is a tool from Microsoft that allows you to record system and application events, that you can then analyse using the Windows Performance Analyzer (WPA). When it comes to memory, they can be used to record VirtualAlloc usage, take Heap Snapshots, track Heap and Pool usage and more. Both tools are part of the Windows Assessment and Deployment Kit (ADK).
The Windows Performance Recorder can also be used to investigate Untracked Memory usage in Unity, as described in this page.
This content originally appeared on DEV Community and was authored by Attilio Carotenuto