Performance of empty Godot callbacks C#

Godot v4.x - Mono C#

There are shorter version of questions at the end.
But figures provide some context to those questions.

Figure 1 - Question

At least before compilation, Godot Node provides virtual methods to override, like:

public override void _Process(double delta)
{
	base._Process(delta);
}

And there are a lot of them.
How does Godot handle those empty virtual methods?
Are they still called, adding overhead, or stripped away?
(via Godot means, or C# JIT compiler)

Also, consider following:

Figure 2 - JIT dead code elimination

public partial class Entity : Node2D
{
	public override void _Ready()
	{
		base._Ready();
	}
}
public sealed class Tank : Entity
{
	public override void _Ready()
	{
		base._Ready();
	}
}

In regular C#, if you use (for whatever reason):

Tank tank = ...;
tank._Ready();

JIT might simply remove calls to _Ready() in a process of Dead Code Elimination.

However, I assume Godot calls C# methods from C++ environment?

If it’s true - does JIT optimizations still apply (dead code elimination, devirtualization, etc.), or does Godot uses a mechanism, bypassing JIT optimizations, thus C# code will have a performance as if compiled in a DEBUG mode? (with some asterisks)

Figure 3 - JIT in with dynamically loaded DLLs

Right now, I’ve stated 3 possibilities:

  • Godot uses custom way of calling C# code, bypassing JIT - no optimizations.
  • Godot harmonizes with C# JIT, allowing it to provide dead code elimination for empty C# methods.
  • Godot reimplements JIT, potentially stripping away unused methods.

If 3rd option is the case - what happens if a mod (.pak/.zip) loads-in a C# DLL? Consider following:

// Game.DLL
public partial class Tracker : Node
{
	public override void _Process(double delta) {}
}

Here, we have “Tracker” with empty _Process method - potential candidate method to be completely removed/not called.

But then, the mod adds functionality to it:

public sealed partial class PlayerTracker : Tracker
{
	const EntityTags Any = EntityTags.Enemy | EntityTags.Ally | ...;
	public override void _Process(double delta)
	{
		// Some player tracking logic:
		using var list = EntitySystem.LocateAll<Player>(tags: Any);
		foreach (Player p in list)
		{
			GD.Print($"Located player #{p.ID}: {p.DislayName}");
		}
	}
}

Now “PlayerTracker._Process(double)” has some code.
While Godot might have saw that “Tracker._Process(double)” has none.
So depending on how it is implemented, all calls from Godot C++ to “Tracker._Process(double)” might haave been elimintated, meaning that “PlayerTracker._Process(double)” will never be called as well.

Conclusion

Considering that Godot devs are smart - I doubt they will ever allow Godot C++ engine to eliminate calls to methods, which can be extended/inherited in mod libraries.

So I assume Godot might avoid calling _Process(delta) method if class does not override it.

But as soon as you override it, engine will call it, even if method is empty:

// Also, don't know if sealed keyword will do anything here.
//  or in the method, like `sealed override`.
public partial class MyNode : Node
{
	// Assuming: enough to force Godot to start calling this method.
	public override void _Process(double delta) {}
}

And whether method should be called is associated with System.Type of the node.

But in short

Does Godot call:

_Process(double), _PhysicsProcess(double), _Notification(int), _EnterTree(), _ExitTree(), _Input(InputEvent), _ShortcutInput(InputEvent), etc.

If nothing explicitly overrides them? Like:

public partial class MyNode : Node
{
	public override void _Process(double delta) {}
	public override void _PhysicsProcess(double delta) {}
	public override void _Notification(int what) {}
	public override void _Input(InputEvent @event) {}
}

And how much performance impacts does it have?
Considering that Godot has to support dynamic DLL loading from .pak/.zip (i.e. Assembly.Load).

You can always measure it. Probably near zero.

Virtual methods get compiled regardless of whether they are used or not. They just end up with a empty block for that method. So it would add to the source code size, but there is no overhead other than that (assuming its not being called anywhere). Either that version of the method will get called or it wont.

Not sure about the rest.

To measure it is a very good idea to be honest…

Will do it soon! ^-^

But impact is never near zero unless the method is inlined. In hot paths (key - hot paths though), method code itself can be loaded in L1-L3 caches, but otherwise resides in RAM. Fetching RAM can be expensive (a.k.a. cache miss), so infrequent calls to random classes, in large projects at least, can have impact on CPU.

It’s negligible if you have only a 1000 or so of those “stray” objects, but you can easily exceed that.

For example - _Notification and _Input can be triggered a lot of times on UI components, or even in the world. I wonder if all of them unnecessarily handle all those events.

Because they can be avoided on Engine-level using System.Type. This is how evil-evil Unity optimize it (so called “magic methods”)

The only saving grace is that non-overriden methods belong to Node. So all its virtual methods only loaded in L1-L3 caches once. But virtual methods are called via vtable, unique to each class, so even if you don’t override a method - system will still go through vtable, which has to be loaded in L1-L3 caches as well.

And only JIT is able to “devirtualize” the call to a method, calling a specific method directly instead of using vtable.

Compiler also can do it, but only if you seal the class or a method ahead of time, and only use final/sealed class in your code.

Yeah, agree that there’s no performance overhead if method is unused. It will take a few bytes in RAM, but never be loaded into L1-L3 caches, and will never be executed.

But what I’m worried about is that Godot calls those callbacks, even if you don’t override the method.

Probably a good idea to measure it, how normalized mentioned.

You should generally have as few per frame sctipt callbacks as possible. Engine lets you explicitly toggle them via Node::SetProcess*() calls. So you can make them not run even if they are implemented.

Oh! That’s amazing! You are right!

Completely forgot about it)

Interesting question. I think the distinction between an empty method and the overhead of calling it is worth considering here. Even if the JIT optimizes away the actual method body, Godot might still have some work to do each frame. I imagine that could become noticeable with thousands of nodes, though I’m not sure how much of it Godot already optimizes internally.

Exactly.

I know Particle systems and similar do not have this issue, because Engine removes unnecessary callbacks from each particle object (and most of the time, just renders mesh itself without actual object)

That’s why general advice is to use them instead of individual objects.

But in large RTS games, like Beltfed, there can be a ton of objects. Devs of such games will have to reinvent a way to avoid unnecessary calls, just to avoid excessive callbacks on random objects, if it’s the case.

While it can be implemented on Engine-level, benefitting everyone

You’re probably over estimating how fine grained control you have with C#. It’s not an AOT compiled language, at least not to machine code, C# still runs on a VM. With how many side effects and it’s capability for reflection I highly doubt C# would ever delete a method even if empty, especially in Godot.

Godot’s underlying system attempts to hit the cache often on important high volume systems. Ultimately, make your game, and use the profiler.

If you do need to bring down frame times, and it’s specifically from engine level issues navigating the scene tree then you can use the underlying systems more directly with RIDs depending on the system.

Oh! RIDs together with generics can optimize away so much)

Amazing!

But as a test, I will check if Godot still continues to call those callbacks. Will come back with the results later.

Though as normalized mentioned, you can already prevent _Process(double) and _PhysicsProcess(double) from being called by tweaking settings on each Node separately.

Those callbacks are the most numerous usually.