Technical Note TN2434

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.

Introduction
Profiling the app for memory usage
Analyzing the results
Conclusion
Related Material
Document Revision History

Introduction

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.

Terms Definition

  • Memory Footprint refers to the total current amount of system memory that is allocated to your app.

Profiling the app for memory usage

  1. Xcode > Product menu > Profile

  2. Choose Allocations.

    Art/tn2434_allocationsInstrument.pngArt/tn2434_allocationsInstrument.png

  3. Click the record button.

    Art/tn2434_recordButton2.png

  4. 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.

  5. Use the app for a few minutes to allow Instruments to retrieve a representative sample of data.

  6. Press the Stop button to stop profiling.

  7. 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.

    Art/tn2434_selectedRange.pngArt/tn2434_selectedRange.png

    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.

    Art/tn2434_selectedRange2.pngArt/tn2434_selectedRange2.png

  8. Now it's time to analyze the results.

    Figure 3 – The results are shown on the Allocations Summary pane.

    Art/tn2434_resultsPane_sized.pngArt/tn2434_resultsPane_sized.png

    Figure 3 Legend:

    1. 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 VM represents 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.

    2. # 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.

  1. Have a look at the first allocation type:

    • Figure 4 – The first allocation type is titled "Malloc 96.00 KiB".

      Art/tn2434_item1.pngArt/tn2434_item1.png

      Figure 4 Legend –

      1. 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%!

      2. 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.

      Art/tn2434_item1_responsibleCaller.pngArt/tn2434_item1_responsibleCaller.png

      Figure 5 Legend –

      1. The first 10 instances of the 144 allocations source from TileSet readImageWithTileset:.

      2. The rest (10 thru 144) source from TileSet checkAndAllocateTileSlice:.

    • 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.

      Art/tn2434_item1_inspectCulprit.pngArt/tn2434_item1_inspectCulprit.png

    • Figure 7 – Show the Extended Detail Pane to view the stack trace where the allocation occurred.

      Art/tn2434_item1_stackTrace.pngArt/tn2434_item1_stackTrace.png

      Figure 7 Legend –

      1. Click to show the Right-pane.

      2. Click the "E" icon to reveal the Extended Detail pane.

      3. View the stack trace and note the call to TileSet checkAndAllocateTileSlice:.

    • Figure 8 – Double click the TileSet checkAndAllocateTileSlice: line in the stack trace to reveal the location within your code the allocation occurred.

      Art/tn2434_item1_doubleClickStackLine.png

      Figure 9 – Line of code where the allocation occurs.

      Art/tn2434_item1_culpriteLineOfCode.pngArt/tn2434_item1_culpriteLineOfCode.png

      • 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:

      1. 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.

      2. Reduce the number of allocations

        For levels that do use masking, the developer might consider reducing the number of allocations. Having observed that the malloc effecting the allocation is repeated kNumberOfTileSices times, the number of allocations can be reduced if kNumberOfTileSlices can be reduced.

        kNumberOfTileSlices controls 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.

      3. 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.

  2. Next, let's have a look at the second largest allocation.

    • Figure 10 – The next allocation is "Tile".

      Art/tn2434_item2.pngArt/tn2434_item2.png

      Figure 10 Legend –

      1. Click the Allocation Summary breadcrumb to return to the results pane.

      2. 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 kNumberOfLayers which in this run was equal to 7.

      Art/tn2434_item2_tileImpl.png

    • Consider ways to reduce this allocation.

      1. 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.

      2. 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.

      3. 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:

        1. 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.

        2. 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.

Conclusion

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.

Related Material

For other memory related tasks, see:

Instruments User Guide > Profile Your App's Memory Usage.



Document Revision History


DateNotes
2016-05-23

New document that walks through the process of minimizing an app's memory usage using the Allocations Instrument.