Minimizing your app's Memory Footprint
For the benefit of both stability and performance, it's important to understand and heed the differing amounts of memory that are available to your app across the various devices your app supports. Minimizing the memory usage of your app is the best way to ensure it runs at full speed and avoids crashing due to memory depletion in customer instantiations. Furthermore, using the Allocations Instrument to assess large memory allocations within your app can sometimes be a quick exercise that can yield surprising performance gains.
This guide provides a walkthrough of the steps to profile your app's memory usage in Xcode. Organizing your app's more significant memory use at a granular level can sometimes yield surprising results. Taking further steps to reduce big allocations is one of the easiest ways to increase performance, and safeguard your app from termination in low memory conditions.
Memory Footprint refers to the total current amount of system memory that is allocated to your app.
Profiling the app for memory usage
Xcode > Product menu > Profile
Click the record button.
Navigate to the area of the app that you're interested in assessing memory usage, such as the main level of a game, or the primary editing operations of an image editing app.
Use the app for a few minutes to allow Instruments to retrieve a representative sample of data.
Press the Stop button to stop profiling.
Select a range spanning the time within the run you want to analyze. Doing this filters the results shown on the Allocations Summary below.
Figure 1 – Hold the Command key and click from areas marked 1 to 2 in order to select the range of interest.
Figure 2 – Include in the range ramps that represent allocation. The range shown here includes allocations for a game's intro screen and loading the first level. These two events were chosen because they allocate a majority of the total memory used by the app, and therefore, they are also the best candidates to speed up.
Now it's time to analyze the results.
Figure 3 – The results are shown on the Allocations Summary pane.
Figure 3 Legend:
Persistent Bytes is the primary focus of this tutorial. It is the total number of bytes your app currently holds in memory. For the purposes of this guide, Persistent Bytes for
All Heap & Anonymous VMrepresents your app's memory footprint.
This is the value to minimize; the lower the persistent bytes, the faster your app can run and the less chances there are to face memory depletion.
# Persistent is the other interesting metric we look at here. It represents the total number of repetitions of a particular allocation.
• Instruments User Guide > Allocations Instrument > Detail Pane Columns.
Analyzing the results
This section analyzes the results of the memory profiling done in the above section, Profiling the app for memory usage.
Have a look at the first allocation type:
Figure 4 – The first allocation type is titled "Malloc 96.00 KiB".
Figure 4 Legend –
This allocation is a Malloc of size 96 KB. 144 of these allocations result in 13.5 MB out of the total 39 MB of our memory footprint. That's a whopping 34%!
Click the right-arrow next to this allocation in order to list all 144 repetitions.
Figure 5 – Now sort the allocation by Responsible Caller to narrow down the locations where it occurs.
Figure 5 Legend –
The first 10 instances of the 144 allocations source from
The rest (10 thru 144) source from
Figure 6 – Select a row whose Responsible Caller repeats the most times. This is the method to analyze first since it composes a majority of this type of allocation.
Figure 7 – Show the Extended Detail Pane to view the stack trace where the allocation occurred.
Figure 7 Legend –
Click to show the Right-pane.
Click the "E" icon to reveal the Extended Detail pane.
View the stack trace and note the call to
Figure 8 – Double click the
TileSet checkAndAllocateTileSlice:line in the stack trace to reveal the location within your code the allocation occurred.
Figure 9 – Line of code where the allocation occurs.
Line 350 is the allocation of interest. It is responsible for 12.56 MB of this type of allocation.
Line 348 is a different allocation, and at 1 KB, it pales in comparison and can be ignored for the time being.
Consider ways to reduce this allocation
Because this allocation accounts for 34% of the app's total memory, it's a single line of code that offers the largest opportunity to minimize memory usage. This line of code is responsible for a feature called "tile slices," a rudimentary geometry-based form of image masking. Given this information, consider the following approaches to minimize this allocation:
Remove the allocation
This geometry-based form of image masking was created with the intention of being incredibly fast at runtime, which it would be if the memory required to support it were not so large. The developer should consider switching to image-based masking, and then profile to assess the new speed-versus-space trade off.
All 34% of the memory footprint can be saved for levels not using image masking if a flag is used to prevent these placeholder allocations in those cases.
Reduce the number of allocations
For levels that do use masking, the developer might consider reducing the number of allocations. Having observed that the
malloceffecting the allocation is repeated
kNumberOfTileSicestimes, the number of allocations can be reduced if
kNumberOfTileSlicescan be reduced.
kNumberOfTileSlicescontrols the number of image masking options to shape a tile; if some of the shapes are used less often, the developer could consider removing the lesser used options.
Reduce the size of the allocation
Alternatively, the developer can look into reducing the size of the allocation. Noticing that the size of the allocation is a factor a TileSet's number of tiles,
numTiles, now is good time to consider whether extra tiles in the TileSet could be removed.
Next, let's have a look at the second largest allocation.
Figure 10 – The next allocation is "Tile".
Figure 10 Legend –
Click the Allocation Summary breadcrumb to return to the results pane.
The second largest allocation made by the app is the Tile class. Though each Tile is relatively small (183 bytes), there are 41,900 instances in memory at one time and that composes a total of 7.67 MB. This allocation is 19.7% of the app's total memory footprint.
For this allocation type, viewing the line of code it sources from is less interesting than considering its overall size and number of repetitions. Opening up Tile.m in the Xcode editor, you can see its 183 bytes are composed of a few handfuls of primitives, most notably, arrays of size
kNumberOfLayerswhich in this run was equal to 7.
Consider ways to reduce this allocation.
Remove the allocation
In this case, removing the Tile allocation is not an option, as it is the most essential data structure required to render a game level.
Reduce the number of allocations
Reducing the number of allocations is more of a level-design choice, than one of programming. This is because the number of Tiles is directly dependent on the designer's choice of map dimensions in the game editor. Therefore, reducing the number of Tile allocations is easier to do moving forward (as a recommendation to the level designer), as retroactive reduction of tiles would likely involve modifying game levels.
Reduce the size of the allocation
Reducing allocation size is the most realistic option the developer can explore to reduce the memory used by Tiles. Here are a couple examples:
The red, green and blue instance variables implement a feature that allows each tile to be colorized dynamically. With
kNumberOfLayers= 7, it costs 3 (bytes) x 7 (number of layers) = 21 bytes out of the 183 bytes for each Tile to implement colorization. If the colorization feature were non-essential or sparsely used, the developer could consider removing it.
How much memory does that earn us? Each tile allocation with colorization removed is 183 bytes - 21 bytes = 162 bytes. 162 bytes x 41,900 (number of tiles) = 6.79 MB.
The total memory savings with colorization removed is 7.67 MB - 6.79 MB = 880 KB, almost 1 MB.
The array of AnimatingTile at line 23 has repetitions for each layer (or z-position). AnimatingTile is an object that allows a Tile's artwork to animate, and because most animations cover the entire size of a tile, a fairly reasonable consolation is to constrain each Tile to only a single AnimatingTile. This removes the array of
AnimatingTile*down to a single pointer, and on 64-bit architecture each memory address is large (8 bytes) when every byte counts.
How much memory does that earn us? Each tile allocation with animating tiles constrained to 1-per-tile is 183 bytes - 6 (number of layers minus one) * 8 bytes (per 64-bit memory address) = 159 bytes. 159 bytes x 41,900 (number of tiles) = 6.66 MB.
The total memory savings is 7.67 MB - 6.66 MB = 1.01 MB savings.
The two changes made above save the app almost 2 MB of memory use, which amounts to a total memory savings of 5% of the app's memory footprint, and we were just getting started.
In the example shown here, we consider only the first two allocations in detail; because we've sorted the allocations by size, the first two account for 50% of the app's total footprint. The next steps are to continue down to the remaining 50%. With each allocation, consider whether it can be removed, reduced in size, or reduced in repetition.
Each app will have different allocation types sourcing from varying locations in code, along with different circumstances around their potential minimization, but the approach to assess memory use remains the same.
For other memory related tasks, see:
• Instruments User Guide > Profile Your App's Memory Usage.
Document Revision History
New document that walks through the process of minimizing an app's memory usage using the Allocations Instrument.