Asset Bundles vs. Resources: A Memory Showdown
Lately we’ve gotten a lot of inquiries about using Asset Bundles over the old school Resources system and why loading an Asset from the former has a higher memory overhead than the latter.
For those of you just looking for the TLDR: They don’t, or rather in the long run Asset Bundles will have a much lower memory overhead if you take advantage of what they can do that the Resources system cannot. If you are not familiar with Asset Bundles and want to learn more, you can refer to the Unity Manual and the Asset Bundles & Resources Guide.
Jumping right in, we received several bug reports all basically saying the same thing: When I load an Asset from an Asset Bundle I see my memory usage jump several megabytes but I do not see this jump when using Resources. In reproducing the steps of the bug we noticed very similar results: normal memory usage at the start, the memory jump when an Asset is loaded, and then noticed that the memory doesn’t return to the original value.
Memory usage for Asset Bundles
Memory usage for Resources
So, before we get into the actual numbers, let’s talk about the system that gives us those numbers and how they relate to each other. Unity’s native memory system uses several fixed-size block allocators that vary in size from 1 MB to 32 MB (avg. 1 MB to 4 MB) depending on what type of work they are assigned to do, such as main thread vs. background thread, or what’s the current running platform. The Reserved Total is the sum of all blocks allocated by the OS, while Used Total is what is actively being used by Unity out of the Reserved Total. Each of the area labels, FMOD, Profiler, etc. represent a set of those allocators or estimated external memory reported by the system. You can read about the area labels on our Memory Profiler manual page. There are a few things to note that are not on the manual page: Used and Reserved totals do not include FMOD values at the time of writing this blog post (Unity 5.5.0f3). We have a fix for this on it’s way. Total System Memory Usage is the system reported virtual memory size by the platform, which is not a feature of all platforms and 0 is displayed in those cases. Finally, Used Total does not take into account an object’s header or byte alignment, however Reserved Total does. So for looking at memory usage of Asset Bundles vs. Resources, we will focus on both the Used Total and Reserved Total sections for the Unity area label.
Another piece of information that is necessary to understand what is going on in the profiler, is how Asset Bundle and Resources data is laid out on disk. Both Resources and Asset Bundles under the hood are very similar in data structures, in that they both have a single file that contains the serialized data for every object, additional resource files for efficient async loading(textures, audio, etc), and a map of Asset file path to serialized objects to load when requested. Asset Bundles pack those files together in an archive and the map is stored in the serialized data in an Asset Bundle object. Resources stores it’s map in a global singleton called the ResourceManager and the files themselves are just lose on disk. Additionally, unlike the Resources system, Assets don’t have to all be in the same Asset Bundle making it possible to load only a subset of data to control memory usage to a greater degree. More details about Asset Bundle internal structure can be found in the related Unity manual page.
Using our knowledge of the memory and file systems of Unity, we can now talk in detail about what we are seeing with the memory usage values. The first thing that stands out is that the Reserved Unity area for Asset Bundles grew 10 MB, while for Resources this did not grow at all. Why is that? This is actually due to the block allocators mentioned earlier. For this particular memory usage test we used the AsyncBundleLoader.cs behavior which uses Coroutines with Async loading APIs. This is important to note because this combination actually uses different block allocators that, up until this point, have yet to be used. So this 10 MB jump is 2 allocators, allocating their initial blocks of memory, and 1 allocator that needs more memory for the new objects, so it allocates a 4 MB block. Of the 2 new allocators, one is allocating a 2 MB block for Asset Bundle Async loading, the other is allocating a 4 MB block for Type Trees (more on Type Trees later). These blocks sizes are optimized for loading multiple Assets and bundles in parallel. For example, you should be able to load objects from 4 to 5 Asset Bundles at the same time without the the allocators for Asset Bundle Async loading or Type Trees needing new blocks. This of course depends on the Asset Bundle size and compression use, as well as how many unique script types you have in these bundles.
Of those allocation blocks, the 4 MB block for Type Trees is only needed when actively loading objects from Asset Bundles. This block should go away after you are done, however because of how we structured our Coroutine in the example, the AssetBundleRequest object is not being cleaned up by the garbage collector as it’s still in scope. As for the 2 MB block for Asset Bundle Async loading, this is used for threaded read buffers into the Asset Bundle archive, and will go away when there are no more internal references to the bundles. The final 4 MB block will not go away as that block is on the main allocator that is used for all our object storage. Since in a typical project object creation / deletion happens very frequently, we pool memory for reuse instead of releasing it back to the allocator. When you look at the final unloaded Reserved Unity values, you will notice the Asset Bundle case (64.1 MB) is very close to the Resources case (63.3 MB), only due to the order that the allocators obtained new blocks.
So we’ve spent all this time talking about Reserved memory, what about the actual efficiency of using that Reserved memory between Asset Bundles and Resources? This one is actually really easy as this is what the Used Unity area is directly showing us. In the Asset Bundle case, it’s using 21.7 MB of its Reserved memory while Resources is using slightly more, at 22.2 MB. In addition, when we unload, this memory drops to 20.7 MB and 21.2 MB respectively. So Asset Bundles is the clear winner for efficiently utilizing memory. As you may have noticed, Asset Bundle usage after unloading is significantly larger (4.4 MB) than when it started. This is due to memory being pooled for reuse as mentioned previously, so if you reload that Asset Bundle and Asset, it returns right back to the 21.7 MB. In the Resources case, the memory difference between start and unload is due to rounding errors.
The block allocations for Asset Bundles can be reduced, with tradeoffs in performance and backwards compatibility. As mentioned above, a 4 MB block allocation is unavoidable as there just is not enough memory available to load the object. Of the remaining 6 MB, 2 MB is due to using the Async loading APIs. So to avoid that block allocation, you just use the synchronous APIs at the cost of an FPS hitch. The final 4 MB allocation is due to the Type Tree system, and as mentioned above we would go into more detail. This system stores extra data in Asset Bundles, but not in Resources, that is used to make Asset Bundles compatible with a wider range of Unity versions and makes serialization attributes such as FormerlySerializedAs work. This allows you to continue using the same Asset Bundles if you upgrade to a newer version of Unity, or make minor code changes, instead of needing to rebuild them, and in return, make your users redownload entire asset bundles when your game changes. To disable writing this extra data, you pass in the BuildAssetBundleOptions.DisableWriteTypeTree option into the BuildPipeline.BuildAssetBundles API.
Asset Bundles Loaded Synchronous without Type Trees
There you have it, Asset Bundles 1, Resources 0. If you want to reproduce this data yourself, the scripts used in this blog have been uploaded to a Github Gist. It’s currently setup to create 100 of each: Textures, Monobehaviors, and Prefabs and uses a fixed randomization seed, so on your machine it will generate the same output each time you run it (but probably different than mine). Just make sure your Asset Bundle project doesn’t accidentally contain a Resources folder with content, otherwise your memory values will be double what you would expect. Check it out, especially with 300 or even 500 of each.