Search code examples
unity-game-engineprofilerzenject

MASSIVE GC allocation (60+MB) What the hell is going on?


I have a problem that I need your help to solve. I am unity profiler newbie so please bare with me.... I'm currently profiling our game, which is already live and quite large, and I noticed something insane when I was looking at the third frame of the profiler. 62MegaByte GC allocations by Zenject. WTH? I knew Zenject was very performance intensive when it comes to this, but I'm almost certain we screwed up in our project.

The problem is that I can't get to the root cause. When I look at the hierarchy, I see SceneContext.Awake(), but then the trace gets lost. There's not much in there except Initialize, but I can't find or trace the Loading.ReadObject class and methods. I also don't think this massive GC alloc has the route cause in Zenjects route cause. Perhaps we have massively abused something, but currently I am stuck and out of ideas on how to proceed. Any good tips would be VERY appreciated.

Many thanks!

enter image description hereenter image description here

I used Unity's profiler to follow down the hierarchy of method calls until I lost the trace


Solution

  • It is not Zenject, it is Unity.

    In your profiler screenshot:

    Your Profiler Screenshot

    • Yellow rectangle shows the order of loading operations that were triggered by Zenject
    • Red rectangle shows that Zenject only triggered 2 Loading.ReadObject operations, which then resulted in 88102 calls to ReadObjectFromSerialziedFile.

    How did it happen?

    When a game object in a scene, a prefab, or some other asset is referenced by a Unity script in Inspector, it is preloaded as soon as this script is in scope (e.g. when script is in a scene and that scene is loaded, or when it is attached to a prefab and that prefab is loaded), including the case when a script references an asset that has a script that references another asset etc.

    This loading isn't immediate and has to be finalized by Unity before you can use the referenced object. This is exactly what happens here: two calls to Loading.ReadObject trigger Loading.LoadRemainingPreallocatedObjects that finalizes the loading of 88102 referenced objects. Just a guess here, but if you don't have this many references in the objects you load via Zenject, I think Unity just accumulated all the referenced objects in the scene and finalizes them all when the first one is used.

    So your memory consumption isn't because Zenject does something shady, it is because Unity does something shady.

    How to fix?

    If you need all your objects right at the start, there is no direct way to decrease the memory use. That's how Unity works. Though you can probably decrease some memory consumption by making less heavy assets.

    Same is if you can delay some loading but then these objects stay in the scene forever. Your scene will in the end consume as much memory as it does now.

    On the other hand, if you can delay the loading of some assets, and then unload them when they are no more needed, you can use Resources or Addressables to achieve that.

    But there are few things to consider:

    1. All these systems are not made to work together, so if you load the same asset via direct reference, then via Adressables, then via Resources, it will consume 3 times more memory compared to when used via a single system. So you need to carefully decide when to use what and divide your assets into unrelated groups.

    2. Both Resources and Addressables still preload the assets, the differences are when it happens and how easy it is to unload them completely. In Resources there's a problem with preloaded prefabs, they cannot be unloaded with UnloadAsset, and you have to call UnloadUnusedAssets instead (which is not what you want as it is a performance-heavy call). In case of Addressables, they formally have better unloading mechanisms (though there were some similar issues reported for some versions, you have to test it yourself). But Addressables load assets asynchronously. Replacing direct asset references with addressables can be tricky as you need to make your code asynchronous.

    3. Zenject works well with Resources. Addressables (and async binding) support is one of the latest additions, and might not be what you want. E.g. one of Zenject's ideas was to inject async loaders and make your code responsible for waiting. But waiting for all addressable prefabs to load and then combining them into a hierarchy right in the installer can be a better way to go. Luckily, Zenject can be extended in many ways.

    4. Memory fragmentation. Anything loaded and then unloaded increases memory fragmentation. In the end, the app can consume less memory by itself, but more memory as a result of fragmentation. So ideally long-living obejcts should all be loaded before any short-living objects. Or you can use object pools to be more memory efficient and not release the memory at the same time.