What is the benefit of one single MonoBehavior class?

Sometimes I see how Unity programmers use one script that inherits MonoBehavior for almost the entire project. The so-called “Update Managers”. All scripts are subscribed to the queue for execution, and the manager runs all functions, and after execution removes them from the queue. Does this really have any effect on optimization?

Answer

This was one of the optimization technique I analyzed in my thesis.

The Unity engine has a Messaging system which allows the developers to define methods that will be called by an internal system based on their functionalities. One of the most commonly used Messages is the Update message. Unity is inspecting every MonoBehaviour the first time the type is accessed (independently from the scripting backend (mono, il2cpp)) and checks if any of the Message methods are defined. If a Message method is defined then the engine will cache this information. Then if an instance of this type is instantiated then the engine will add it to the appropriate list and will call the method whenever it should. This is also the key reason why Unity does not care about the visibility of our Message method, and that they are not called in a deterministic order.

public class Example1 : MonoBehaviour
{
    private void Update() { }
}
public class Example2: MonoBehaviour
{
    public void Update() { }
}

Both of the above achieves the same results but god knows which will be called first.

One of the main problem with this approach is that every time the engine calls a Message method an interop call (a call from c/c++ side to the managed c# side) has to happen. In case of Update luckily no marshaling is needed so this overhead is a bit smaller. However, if our game handles thousand or tens of thousands of objects which all have a script requiring a Message call then this overhead can be significant. A solution to this is to avoid interop calls. A good approach to this is behavior grouping. If we have a MonoBehaviour that is attached to a huge number of GameObjects we can cut the number of interop calls to just one by introducing an update manager. Since the update manager is also a managed object running managed code the only interop call will happen between the update manager’s Update Message and the Unity engine’s internal Message handler. We have to note the fact that this optimization technique is only relevant in large scale projects, and the frame time saved via this technique is more impactful when using the Mono scripting backend. (Remember IL2CPP transpiles to C++).

test The above picture illustrates the difference between the two methods.

Let’s do a benchmark with Unity’s performance tools. The benchmark will spawn 10 000 gameobjects each with a mover script which moves these cubes up and down.

test Illustration of the example scene using the traditional method.

Now let’s see the results of the bechmarks. enter image description here

Not surprisingly IL2CPP leading the competition by far however it’s still interesting that the Update Manager is still twice as fast as the traditional way. If we profiled the execution of the Traditional method’s IL2CPP build we would find many Unity specific calls like check if the GameObject exists before invoking a component method etc. and these would explain the longer execution time. We can also make the conclusion that IL2CPP in this case is far faster than Mono, usually around twice as fast. The benchmark ran for one minute prior to a 5 seconds warmup and both scripting backends had the ideal compiler setting.