By profiling and honing your game’s performance for a broad range of platforms and devices, you can expand your player base and increase your chance for success.
This page provides information on two tools for analyzing memory usage in your application in Unity: the built-in Memory Profiler module and the Memory Profiler package, a Unity package that you can add to your project.
The information here is excerpted from the e-book Ultimate guide to profiling Unity games, available to download for free. The e-book was created by both external and internal Unity experts in game development, profiling, and optimization.
Read on to learn about memory profiling in Unity.
What is memory profiling?
Memory profiling is important for optimizing your project. Use it to test against hardware memory limitations, decrease loading time and potential out-of-memory crashes, and make your project compatible with a wide variety of devices. It’s also relevant if you want to improve CPU/GPU performance by making changes that actually increase memory usage when there is room to spare, e.g., for object pooling or caching.
There are two ways to analyze memory usage in your application in Unity.
- The Memory Profiler module: This is a built-in profiler module that gives you basic information on where your application uses memory.
- The Memory Profiler package: This tool enables you to take a “snapshot” of your project during runtime and review its memory usage. Use it to dig deep into where memory is being allocated, pinpoint issues, view allocations or memory fragmentation, and compare usage between multiple snapshots. You can view the data from each type of analysis in an Editor window.
With these built-in tools, you can monitor memory usage, locate areas of an application where it’s higher than expected, and run through different workflows to improve memory usage.
Understand and define a memory budget
Understanding and budgeting for the memory limitations of your target devices are critical for multiplatform development. When designing scenes and levels, stick to the memory budget that’s set for each target device. By setting limits and guidelines, you can ensure that your application works well within the confines of each platform’s hardware specification.
You can find device memory specifications in developer documentation. For example, the Xbox One console is limited to 5 GB of maximum available memory for games running in the foreground, according to documentation.
It’s also useful to set content budgets around mesh and shader complexity, and texture compression since they all impact memory allocation. The budgets can then be referred to throughout a project’s development cycle.
Determine physical RAM limits
Set a memory budget for your application by knowing the memory limit of each target platform. Use the Memory Profiler to look at a captured snapshot. The Hardware Resources view (see image above) shows sizes for Physical Random Access Memory (RAM) and Video Random Access Memory (VRAM). This figure doesn’t account for the fact that not all of that space might be available to use. However, it provides a useful ballpark figure to start working with.
It’s a good idea to cross-reference hardware specifications for target platforms since figures displayed here might not always show the full picture. Developer kit hardware sometimes has more memory, or you may be working with hardware that has a unified memory architecture.
Determine the lowest RAM specification
Identify the hardware with the lowest specification in terms of RAM for each platform you support, and use this to guide your memory budget decision. Remember that not all of that physical memory might be available to use. For example, a console could have a hypervisor running to support older games which might use some of the total memory. Think about a percentage (e.g., 80% of total) to use. For mobile platforms, you might also consider splitting into multiple tiers of specifications to support better quality and features for those with higher-end devices.
Consider per-team budgets
Once you have a memory budget defined, consider setting memory budgets per team: Your environment artists get a certain amount of memory to use for each level or scene that is loaded, the audio team gets memory allocation for music and sound effects, and so on.
You might have to be flexible with the budgets as the project progresses. If one team comes in under budget, assign the surplus to another team if it can improve the areas of the game they’re developing.
The next step after setting memory budgets for your target platforms is to use profiling tools to help you monitor and track memory usage in your game.
Two views with Memory Profiler module
The Memory Profiler module provides two views: Simple and Detailed. Use the Simple view to get a high-level view of memory usage for your application and switch to the Detailed view to drill down further.
The Total Reserved Memory figure is the Total Tracked by Unity Memory. It includes memory that Unity has reserved but isn’t using right now (that figure is the Total Used Memory).
The System Used Memory figure is what the OS considers as being in use by your application. Be aware that if this figure displays 0, it’s indicating that the Profiler counter is not implemented on the platform you are profiling. In this case, the best indicator to rely on is Total Reserved Memory, as well as switching to a native platform profiling tool for detailed memory information.
Detailed view in Memory Profiler
Frame-by-frame memory figures don’t provide enough information about how much memory is used by your executable, DLLs, and the Mono Virtual Machine. You’ll need to use a Detailed snapshot capture to dig into this type of memory breakdown.
Note: Newer versions of Unity may direct you to use the Memory Profiler package (instead of the module) for capturing and analyzing memory snapshots in the Detailed view. The reference tree in the Detailed view of the Memory Profiler module only shows Native references. References from objects of types inheriting from UnityEngine.Object might show up with the name of their managed shells. However, they might show up only because they have Native Objects underneath them. You won’t necessarily see any managed type.
Let’s take as an example an object with a Texture2D in one of its fields as a reference. Using this view, you won’t see which field holds that reference, either. For this kind of detail, use the Memory Profiler package.
To determine at a high-level when memory usage begins to approach platform budgets, use the following “back-of-napkin” calculation:
System Used Memory (or Total Reserved Memory if System Used shows 0) + ballpark buffer of untracked memory / Platform total memory
When this figure starts approaching 100% of your platform’s memory budget, use the Memory Profiler package to figure out why.
Many of the features of the Memory Profiler module have been superseded by the Memory Profiler package, but you can still use the module to supplement your memory analysis efforts.
- To spot GC allocations: Although these show up in the module, they are easier to track down using Project Auditor or Deep Profiling.
- To quickly look at the Used/Reserved size of the heap
- Shader memory analysis
Remember to profile on the device that has the lowest specs for your overall target platform when setting a memory budget. Closely monitor memory usage, keeping your target limits in mind.
You’ll usually want to profile using a powerful developer system with lots of memory available (space for storing large memory snapshots or loading and saving those snapshots quickly is important).
Memory profiling is a different beast compared with CPU and GPU profiling because it can incur additional memory overhead itself. You may need to profile memory on higher-end devices (with more memory), but watch out for the memory budget limit for the lower-end target specification.
Points to consider when profiling for memory usage:
- Settings such as quality levels, graphics tiers, and AssetBundle variants may have different memory usage on more powerful devices. For example:
- The Quality Level and Graphics settings could affect the size of RenderTextures used for shadow maps.
- Resolution scaling could affect the size of the screen buffers, RenderTextures, and post-processing effects.
- Texture quality setting could affect the size of all textures.
- The maximum LOD could affect models and more.
- If you have AssetBundle variants like a HD (High Definition) and SD (Standard Definition) and choose which one to use based on the device specifications, you might also get different asset sizes based on which device you’re profiling on.
- The screen resolution of your target device will affect the size of RenderTextures used for post-processing effects.
- The supported Graphics API of a device might affect the size of shaders based on which of their variants are supported or not by the API.
- Having a tiered system that uses different quality settings, graphic tier settings, and AssetBundle variations is a great way to be able to target a wider range of devices, e.g., by loading a HD version of an AssetBundle on a 4 GB mobile device, and a SD version on a 2 GB device. However, take the above variations in memory usage in mind, and make sure to test both types of devices, as well as devices with different screen resolutions or supported graphics APIs.
Note: The Unity Editor will generally show a larger memory footprint due to additional objects that are loaded from the Editor and Profiler. It may even show asset memory that would not be loaded into memory in a build, such as from AssetBundles (depending on the Addressables simulation mode), sprites and atlases, or for assets shown in the Inspector. Some of the reference chains may also be more confusing in the Editor. Be sure to capture snapshots using standalone platform development builds rather than from the Editor to get as accurate a snapshot as possible.
The Memory Profiler package
In addition to capturing native objects (like the Memory Profiler module does), the Memory Profiler package also allows you to view Managed Memory, save and compare snapshots, and explore the memory contents in even more detail.
A snapshot shows memory allocations in the engine, allowing you to quickly identify the causes of excessive or unnecessary memory usage, track down memory leaks, or see heap fragmentation.
Install the Memory Profiler package, then open it by clicking Window > Analysis > Memory Profiler.
The Memory Profiler’s top menu bar allows you to change the player selection target and capture or import snapshots. Once a snapshot is loaded, the main view is divided into three workflow views, or tabs: Summary, Unity Objects, and All of Memory.
Note: Profile memory on target hardware by connecting the Memory Profiler to the remote device with the Target selection dropdown. Profiling in the Unity Editor will give you inaccurate figures due to overheads added by the Editor and other tooling.
Single and Compare snapshots
On the left of the Memory Profiler window is the Workbench area. Use this to manage and open or close saved memory snapshots. Use this area to also switch between Single and Compare Snapshots views.
Memory Profiler allows you to load two data sets (memory snapshots) to compare them. This is especially useful when looking at how memory usage grew over time or between scenes and when searching for memory leaks.
The tabs Summary, Unity Objects, and All of Memory allow you to dig into memory snapshots, quickly switching between views as you progress through your memory profiling workflows. Let’s look at each of these options in detail.
The Summary view
This is the default view that opens when you load or capture a snapshot and is a good choice to start a memory profiling task. The Summary view provides information on how much memory you are using, how much is “resident” on the device, and how much is committed but not currently on device. It also provides information about how memory is distributed across categories, helping you to choose where to start your investigation.
Unity Objects view
The Unity Objects view is the bread and butter of any memory profiling or analysis session. You’ll most likely spend most of your time examining memory with this view. It lists all the main types of objects loaded in memory, such as textures, shaders, audio, and so on.
This view can help you identify assets that are too big or don’t need to be loaded at certain points, either because they were loaded unnecessarily, or because they were kept in memory by a hanging reference.
All of Memory view
Once you identify an area of your project to examine, you’ll typically switch to the All of Memory view.
This view enables you to see all memory, divided up into categories: Native, Executables and Mapped, Graphics, and Scripting.
Each of these categories allows you to see the actual data or assets consuming memory. Selecting an item in this view opens a detailed information panel to the side.
In the case of Unity Objects, the detailed view can show you references from or to the asset, as well as a plethora of other useful information to figure out what kind of memory lifecycle it has, including instance identifier information, managed fields (for scripts) along with their individual sizes, and more.
Memory Profiler snapshot comparison
One of the big benefits of the Memory Profiler package is the ability to compare A and B scenarios of your application’s lifecycle using multiple memory snapshots.
The Snapshot list is used to highlight and load two snapshots, at which point the detailed views (Summary, Unity Objects, and All Of Memory) change to support a differential view called Compare mode.
In Compare mode, snapshots are labeled “A” and “B.”
Selecting the Summary view provides the two captures’ memory breakdowns, side by side, so you can see the main differences in memory usage between the snapshots.
The Unity Objects and All Of Memory views have a dedicated UI that allows you to see how different memory categories have changed in size, or how counts have changed between snapshot captures. The tables illustrate differences with sorted columns showing size difference bar charts, figures, and counts.
Selecting items in the top table view will allow you to inspect the individual differences between the Base (“A”) and Compared (“B”) snapshot.
For example, the same item might show different Total Size values between captures. Selecting this item for comparison allows you to use the Selection Details view to the right side to examine in closer detail, where you might find a field reference has grown significantly between snapshots.
Pinpoint memory-heavy assets
Load a Memory Profiler snapshot and go through the Unity Objects view to inspect the list of types, ordered from largest to smallest in memory footprint size.
Project assets often consume the most memory. Using the Table view, locate texture objects, meshes, AudioClips, RenderTextures, and shaders, among others. These are all good candidates for memory optimization.
The Summary view also has some at-a-glance memory breakdowns such as the Top Unity Objects categories, which is useful for pinpointing where and how memory is committed in a snapshot.
Locating memory leaks
A memory leak typically happens when:
- An object is not released manually from memory through the code
- An object stays in memory because of an unintentional reference
Compare mode in the Memory Profiler package can help find memory leaks by comparing two snapshots over a specific timeframe. For example, a common memory leak scenario in Unity games can occur after unloading a scene.
The Memory Profiler package documentation provides a workflow that guides you through the process of discovering these types of leaks using the Compare mode.
Locating recurring memory allocations over application lifetime
Through differential comparison of multiple memory snapshots, you can identify the source of continuous memory allocations during application lifetime.
The following sections list some tips to help identify managed heap allocations in your projects.
Locating memory allocations
The Memory Profiler module in the Unity Profiler represents managed allocations per frame with a red line. This should be 0 most of the time, so any spikes in that line indicate frames you should investigate for managed allocations.
Timeline view in the CPU Usage Profiler module
The Timeline view in the CPU Usage Profiler module shows allocations, including managed ones, in pink, making them easy to hone in on.
Allocation call stacks
Allocation call stacks provide a quick way to discover managed memory allocations in your code. They show you the call stack detail you need at less overhead compared to deep profiling, and they can be enabled on the fly using the standard Profiler.
Allocation call stacks are disabled by default in the Profiler. To enable them, click the Call Stacks button in the main toolbar of the Profiler window. Change the Details view to Related Data.
Note: If you’re using an older version of Unity (prior to Allocation call stack support), then deep profiling is a good way to get full call stacks to help find managed allocations.
GC.Alloc samples selected in the Hierarchy or Raw Hierarchy will now contain their call stacks. You can also see the call stacks of GC.Alloc samples in the selection tooltip in Timeline.
The Hierarchy view in the CPU Usage Profiler
The Hierarchy view in the CPU Usage Profiler lets you click on column headers to use them as the sorting criteria. Sorting by GC Alloc is a great way to focus on those.
Project Auditor is an experimental static analysis tool. It does a lot of useful things, several of which are outside the scope of this guide, but it can produce a list of every single line of code in a project which causes a managed allocation, without ever having to run the project. It’s a very efficient way to find and investigate these sorts of issues.
Memory and GC optimizations
Unity uses the Boehm-Demers-Weiser garbage collector, which stops running your program code and only resumes normal execution once its work is complete.
Be aware of unnecessary heap allocations that can cause GC spikes:
- Strings: In C#, strings are reference types, not value types. This means that every new string will be allocated on the managed heap, even if it’s only used temporarily. Reduce unnecessary string creation or manipulation. Avoid parsing string-based data files such as JSON and XML, and store data in ScriptableObjects or formats like MessagePack or Protobuf instead. Use the StringBuilder class if you need to build strings at runtime.
- Unity function calls: Some Unity API functions create heap allocations, particularly ones which return an array of managed objects. Cache references to arrays rather than allocating them in the middle of a loop. Also, take advantage of certain functions that avoid generating garbage. For example, use GameObject.CompareTag instead of manually comparing a string with GameObject.tag (as returning a new string creates garbage).
- Boxing: Avoid passing a value-typed variable in place of a reference-typed variable. This creates a temporary object, and the potential garbage that comes with it implicitly converts the value type to a type object (e.g., int i = 123; object o = i). Instead, try to provide concrete overrides with the value type you want to pass in. Generics can also be used for these overrides.
- Coroutines: Though yield does not produce garbage, creating a new WaitForSeconds object does. Cache and reuse the WaitForSeconds object rather than creating it in the yield line or use yield return null.
- LINQ and Regular Expressions: Both of these generate garbage from behind-the-scenes boxing. Avoid LINQ and Regular Expressions if performance is an issue. Write for loops and use lists as an alternative to creating new arrays.
- Generic Collections and other managed types: Don’t declare and populate a List or collection every frame in Update (for example, a list of enemies within a certain radius of the player). Instead, make the List a member of the MonoBehaviour and initialize it in Start. Simply empty the collection with Clear every frame before using it.
Time garbage collection whenever possible
If you are certain that a garbage collection freeze won’t affect a specific point in your game, you can trigger garbage collection with System.GC.Collect.
See Understanding Automatic Memory Management for examples of how to use this to your advantage.
Use the Incremental Garbage Collector to split the GC workload
Rather than creating a single, long interruption during your program’s execution, incremental garbage collection uses shorter interruptions that distribute the workload over many frames. If garbage collection is causing an irregular frame rate, try this option to see if it can reduce the problem of GC spikes. Use the Profile Analyzer to verify its benefit to your application.
Note that using the GC in Incremental mode adds read-write barriers to some C# calls, which comes with some overhead that can add up to ~1 ms per frame of scripting call overhead. For optimal performance, it’s ideal to have no GC Allocs in the main gameplay loops so that you don’t need the Incremental GC for a smooth frame rate, and can hide the GC.Collect where a user won’t notice it, for example, when opening the menu or loading a new level.
To learn more about the Memory Profiler and further understand how to manage your application’s memory lifecycles, check out the following resources: