Unity Scene Loading Times

Preface

While I’ve been working on my last project with many objects being loaded initially on the scene, I started to wonder how should I handle their instantiation. Should I just drag’n’drop them and disable, so they don’t disturb or annoy me during further development? Should I gather all these objects onto some kind of list and instantiate them at scene start? Which way is more elegant? And the most important, which is the fastest? Instead of guessing I just prepared few test case scenarios and examined gathered data.

Unity Scene

Before we jump into testing, let’s describe what unity scene actually is. If you have never opened .unity scene file I encourage you to take a look at it. It isn’t any kind of magic (a.k.a. binary) file that is unreadable for normal human being. In fact, it’s the exact opposite. Unity scene is serialized in language called YAML which stays for YAML Ain't Markup Language. It’s described as human-readable data serialization language. This already gives us small hint, since it is nothing else but text file (at least for Unity editor).

About test cases

First set of tests is benchmarked directly in Unity editor. Second set of tests is benchmarked in built executable form. Test case scenarios are as following:

I also thought about case with loading prefab from resources folder but when I noticed that first sentence of best practices when using this feature written by Unity is:

Don’t use it.

I have listened to them and didn’t use it. ¯\_(ツ)

What’s worth mentioning before presenting result is that all benchmarks were launched on my notebook with old i5 CPU and SSD disk. Also, game objects and prefabs serialized on scene aswell as those created dynamically were in practice same single game object with one custom component attached to it. It may be relevant during making conclusion later on.

Dots represent actual data and the line represents a trend line.

Editor tests

Editor loading time

Surprised? I was. For some reason I was so sure that Unity does some magic behind our eyes and optimizes scene loading since it already knows what’s on it, right? But let’s face the truth. There is no magic here. It’s pure, serialized data that needs to be read from disk, then deserialized and finally instantiated into the scene. That’s quite a lot of work, isn’t it? On the other hand, we have dynamic creation of object that’s loaded into memory and then instantiated multiple times. Observant reader might notice that since we are using same object dynamically it’s somehow optimized and I bet it’s indeed. My bet is that the object’s stored in CPU cache and is used for every single instantiation. According to this blog post (I highly recommend you to read whole post) reading from L1 cache takes about 3 to 4 CPU cycles while reading from the main RAM is a cost around 100 to 150 CPU cycles. It’s around 35 times slower. After examining the difference between editor and executable benchmarks, the average ratio of game objects instantiation is 4.66 times better in favor of executable form and the average ratio of prefabs instantiation is 21.94 times better in favor of executable form. It’s nowhere close to 35 but we have to remember that there is actually a lot of noise around object instantiation despite reading it’s data from memory or cache.

Scene file size

When comparing scene files sizes of both, serialized game objects and prefabs we can notice better growth tendency for prefabs version is more than twice as big as for game objects version. That’s also interesting, cause, as we all know, prefab is a reference to a single object, so if we instantiate it with default values multiple times it should contain just reference to original object. But it does not. In fact, it has even more data than plain game object because every single serialized property contains original file and game id. For example, serialized position of a prefab looks like this:

    - target: {fileID: 148633973412368616, guid: e0f75b7fa7a6804409c70fa393449fd2, type: 3}
      propertyPath: m_LocalPosition.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 148633973412368616, guid: e0f75b7fa7a6804409c70fa393449fd2, type: 3}
      propertyPath: m_LocalPosition.y
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 148633973412368616, guid: e0f75b7fa7a6804409c70fa393449fd2, type: 3}
      propertyPath: m_LocalPosition.z
      value: 0
      objectReference: {fileID: 0}

On the other hand, serialized game objects position is straight forward and looks like this:

  m_LocalPosition: {x: 0, y: 0, z: 0}

Executable tests

Executable loading time

No exponential growth this time. To be honest, I was relieved when I first saw this cause no matter which approach we choose, we’ll end up with very similar loading times in a final release. The fastest one is prefab instantiation. This matches the results from editor loading times. The slowest one is a game object creation with component attaching during runtime. My guess would be that attaching component is pretty time consuming action and even though both, game object and component script are cached it’s still required to make some computation in order to combine them.

Facts encountered during tests

Slope comparison

Conclusion

In theory all we care about is final user experience but the truth is that the more comfortable environment we are working with the better quality of final product we achieve. Fast iterator is super important. I can’t imagine that everytime when I switch scene in editor I have to wait for like 20 seconds. Knowing that having reasonable amount of objects on scene doesn’t have negative impact on performance aswell as dynamic creation of object (heck, it turned out to be the fastest one, we can mix all of these and focus on creating our work environment to be as convenient for us as possible. At the end I’d like to get back to one of my previous sentence:

… game objects and prefabs serialized on scene aswell as those created dynamically

were in practice same single game object with one custom component attached to it. Smart reader would notice that in practice it’s rather uncommon to load same object so many times. In most cases we load many different objects. This may cause some extra memory read operation since all used objects might not fit into L1 cache and thus general performance might be worse. I’m fully aware of that, nevertheless in my opinion, benchmarks presented in this post are good enough and provided answers I was seeking. I don’t exclude that I might extend this post in the future with more benchmarks.