Search Unity

Share

Is this article helpful for you?

Thank you for your feedback!

Unity has so-called Messaging system which allows you to define a bunch of magic methods in your scripts which will be called at specific events while your game is running. This is a very simple and easy to understand concept especially good for new users. Just define an Update method like this and it will be called once a frame!

void Update() {
    transform.Translate(0, 0, Time.deltaTime);
}

For an experienced developer this code is a bit odd.

  1. It's not clear how exactly this method is called.
  2. It's not clear in what order these methods are called if you have several objects in a scene.
  3. This code style doesn't work with intellisense.

How Update is called

No, Unity doesn't use System.Reflection to find a magic method every time it needs to call one.

Instead, the first time a MonoBehaviour of a given type is accessed the underlying script is inspected through scripting runtime (either Mono or IL2CPP) whether it has any magic methods defined and this information is cached. If a MonoBehaviour has a specific method it is added to a proper list, for example if a script has Update method defined it is added to a list of scripts which need to be updated every frame.

During the game Unity just iterates through these lists and executes methods from it — that simple. Also, this is why it doesn't matter if your Update method is public or private.

In what order Updates are executed

The order is specified by Script Execution Order Settings (menu: Edit > Project Settings > Script Execution Order). It might be not the best way to manually set the order of 1000 scripts but if you want one script to be executed after all other ones this way is acceptable. Of course, in the future we want to have a more convenient way to specify execution order, using an attribute in code for example.

It doesn't work with intellisense

We all use an IDE of some sort to edit our C# scripts in Unity, most of them don't like magic methods for which they can't figure out where they are called, if at all. This leads to warnings and makes it harder to navigate the code.

Sometimes developers add an abstract class extending MonoBehaviour, call it BaseMonoBehaviour or alike and make every script in their project extend this class. They put some basic useful functionality in it along with a bunch of virtual magic methods like so:

public abstract class BaseMonobehaviour : MonoBehaviour {
    protected virtual void Awake() {}
    protected virtual void Start() {}
    protected virtual void OnEnable() {}
    protected virtual void OnDisable() {}
    protected virtual void Update() {}
    protected virtual void LateUpdate() {}
    protected virtual void FixedUpdate() {}
}

This structure makes using MonoBehaviours in your code more logical but has one little flaw. I bet you already figured it out...

All your MonoBehaviours will be in all update lists Unity uses internally, all these methods will be called each frame for all your scripts, mostly doing nothing at all!

One might ask why should anyone care about an empty method? The thing is that these are the calls from native C++ land to managed C# land, they have a cost. Let's see what this cost is.

Calling 10000 Updates

For this post I created a small example project which is available on Github. It has 2 scenes which can be changed by tapping on a device or pressing any key in editor:

(1) In the first scene 10000 MonoBehaviours are created with this code inside:

private void Update() {
    i++;
}

(2) In the second scene another 10000 MonoBehaviours are created but instead of having an Update they have a custom UpdateMe method which is called by a manager script every frame like so:

private void Update() {
    var count = list.Count;
    for (var i = 0; i < count; i++) list[i].UpdateMe();
}

The test project was run on 2 iOS devices compiled to Mono and IL2CPP in non-Development mode in Release configuration. Time was measured as following:

  1. Set up a Stopwatch in the first Update called (configured in Script Execution Order),
  2. Stop the Stopwatch at LateUpdate,
  3. Average the timings over a few minutes.

Unity version: 5.2.2f1
iOS version: 9.0

Mono

WOW! This is a lot! There must be something wrong with the test!

Actually, I just forgot to set Script Call Optimization to Fast but no Exceptions, but now we can see what impact on performance this particular setting has... not that anyone cares anymore with IL2CPP.

Mono (fast but no exceptions)

OK, this is better. Let's switch to IL2CPP.

IL2CPP

Here we see two things:

  1. This particular optimization still makes sense in IL2CPP.
  2. IL2CPP still has room for improvement and as I'm writing this post Scripting and IL2CPP teams are working hard to increase performance. For example the latest Scripting branch contains optimizations making the test run 35% faster.

I’ll explain what Unity is doing under the hood in a few moments. But right now let’s change our Manager code to make it 5 times faster!

Interface calls, virtual calls and array access

If you haven't read this great series of posts about IL2CPP internals you should do it right after you finish reading this one!

It turns out that if you'd wanted to iterate through a list of 10000 elements every frame you'd better use an array instead of a List because in this case generated C++ code is simpler and array access is just faster.

In the next test I changed List<ManagedUpdateBehavior> to ManagedUpdateBehavior[].

This looks much better!

Update: I ran the test with array on Mono and got 0.23ms

Instruments to the rescue!

We figured out that calling functions from C++ to C# is not fast, but let’s find out what Unity is actually doing when calling Updates on all these objects. The easiest way to do this is to use Time Profiler from Apple Instruments.

Note that this is not a Mono vs. IL2CPP test — most of the things described further are also true for a Mono iOS build.

I launched the test on iPhone 6 with Time Profiler, recorded a few minutes of data and selected a one minute interval to inspect. We are interested in everything starting from this line:
void BaseBehaviourManager::CommonUpdate<BehaviourManager>()

If you haven't used Instruments before, on the right you see functions sorted by execution time and other functions they call. The most left column is CPU time in ms and % of these functions and functions they call combined, second left column is self execution time of the function. Note that since CPU wasn't fully used by Unity during this experiment we see 10 seconds of CPU time spent on our Updates in a 60 seconds interval. Obviously we are interested in functions taking most time to execute.

I used my mad Photoshop skills and color coded a few areas for you to better understand what’s going on.

UpdateBehavior.Update()

In the middle you see our Update method or how IL2CPP calls it — UpdateBehavior_Update_m18. But before getting there Unity does a lot of other things.

Iterate over all Behaviours

Unity goes over all Behaviours to update them. Special iterator class, SafeIterator, ensures that nothing breaks if someone decides to delete the next item on the list. Just iterating over all registered Behaviours takes 1517ms out of total 9979ms.

Check if the call is valid

Next, Unity does a bunch of checks to make sure that it is calling a valid existing method on an active GameObject which has been initialized and its Start method called. You don’t want your game to crash if you destroy a GameObject during Update, do you? These checks take another 2188ms out of total 9979ms.

Prepare to invoke the method

Unity creates an instance of ScriptingInvocationNoArgs (which represents a call from native side to managed side) together with ScriptingArguments and orders IL2CPP virtual machine to invoke the method (scripting_method_invoke function). This step takes 2061ms out of total 9979ms.

Call the method

scripting_method_invoke function checks that passed arguments are valid (900ms) and then calls Runtime::Invoke method of IL2CPP virtual machine (1520ms). First, Runtime::Invoke checks if such method exists (1018ms). Next, it calls a generated RuntimeInvoker function for method signature (283ms). It in turn calls our Update function which according to Time Profiler takes 42ms to execute.

And a nice colorful table.

Managed Updates

Now let’s use Time Profiler with the manager test. You can see on the screenshot that there are the same methods (some of them take less than 1ms total so they are not even shown) but most of the execution time is actually going to UpdateMe function (or how IL2CPP calls it — ManagedUpdateBehavior_UpdateMe_m14). Plus, there’s a null check inserted by IL2CPP to make sure that the array we are iterating over is not null.

The next image uses the same colors.

So, what do you think now, should one care about a little method call?

A few words about the test

To be honest, this test is not completely fair. Unity does a great job guarding you and your game from unintended behavior and crashes: Is this GameObject active? Wasn’t it destroyed during this update loop? Does Update method exist on the object? What to do with a MonoBehaviour created during this update loop? — my manager script doesn’t handle anything of that, it just iterates through a list of objects to update.

In real world manager script probably would have been more complicated and slower to execute. But in this case I am the developer — I know what my code is supposed to do and I architect my manager class knowing what behavior is possible and what isn’t in my game. Unity unfortunately doesn’t possess such knowledge.

What should you do?

Of course it all depends on your project, but in the field it’s not rare to see a game using a large number of GameObjects in the scene each executing some logic every frame. Usually it’s a little bit of code which doesn’t seem to affect anything, but when the number grows very large the overhead of calling thousands of Update methods starts to be noticeable. At this point it might already be too late to change the game’s architecture and refactor all these objects into manager pattern.

You have the data now, think about it at the beginning of your next project.

December 23, 2015 in Engine & platform | 9 min. read

Is this article helpful for you?

Thank you for your feedback!

Related Posts