Godot Version
3.5, but any version really.
Question
Does Godot auto inline functions when you export the final product, or is every function called anew? For instance:
func add_two(num: int) -> int:
var add_one = increment(num)
return increment(add_one)
Assuming all increment(num) does is return num+1, would my add_two() function actually call increment() twice, or does it treat it as if “num+1” had been written there instead?
I posted an answer but changed my mind. I missed the ‘on export’. Sorry.
1 Like
There’s a thread on high function overhead, I doubt export is going to add a drastic time-saver like inlining gdscript. If you are worried about such micro-optimizations I would recommend converting this class to a GDExtension where one has micro-optimization control.
Godot Version
v4.3.stable.official
Question
extends Node2D
func _ready() -> void:
var s := Time.get_ticks_msec()
var arr: PackedInt32Array
for i in range(100_000):
arr.append(f(i))
var e := Time.get_ticks_msec()
print(e-s)
func f(i: int) -> int:
var res := i * i
return res
takes ~60-65ms
replace arr.append(f(i)) with arr.append(i * i)
takes ~5-6ms
that is an extreme difference. how come there is such an overhead in calling a function? i dont see this is the other languages i use…
1 Like
I see, thanks. Well, if this is considered a “micro” optimization then I won’t worry about it until it presents itself as a problem.
If performance is important for your project, then GDExtension is one option. Another, probably easier option is to use C#, which automatically inlines function calls where it can be reasonably expected to improve performance. If you’re certain calls to a function should be inlined, you can also use the [MethodImpl(MethodImplOptions.AggressiveInlining)]
attribute to push the JIT harder to inline calls that it otherwise might not.
If you are conscious enough of performance to be asking the question, I recommend using C# instead of GDScript (though this may not work as well with Godot 3.x), and I recommend being careful with how the performance-sensitive code may interface with Godot’s engine API. C# is much more performant than GDScript as a default, while also giving much greater control over things like memory management and function call inlining.
Performance comparison showing how C# is orders of magnitude faster with simple algorithms, not touching the engine API:
https://old.reddit.com/r/godot/comments/1g50mlq/c_vs_gdscript_performance_with_large_for_loops/
Information on the performance implications of how Godot implements its C# engine API bindings:
UPDATE: This article has started an ongoing conversation with the Godot devs. They care about the issues raised, and would like to improve things. Significant improvements will almost certainly be made, although it’s still early days and it’s unclear...
Performance comparison showing that using Node features like _Process
in C# can degrade performance significantly, and so should be used conservatively where performance is a concern:
opened 03:15PM - 14 Oct 24 UTC
closed 07:31PM - 15 Oct 24 UTC
archived
topic:dotnet
performance
### Tested versions
- Reproducible in v4.3.stable.mono.official.77dcf97d8
### … System information
Godot v4.3.stable.mono - Windows 10.0.19045 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 4090 (NVIDIA; 32.0.15.6590) - AMD Ryzen 9 7950X3D 16-Core Processor (32 Threads)
### Issue description
I was getting some terrible performance on my project after adding a C# script to a node that was instantiated around 4000 times, while that sounds like a lot individually the nodes weren't processing anything crazy so I started profiling the issue and found out the problem was indeed adding the `_Process` method to the script that was created 4000 times.
### Steps to reproduce
I didn't take a look at the internals but looks like it's using reflection or something similar to find the `_Process` method on every script and that is extremely slow, here is a comparison:
Here is the script of the main node of the main scene:
```cs
using Godot;
using System.Collections.Generic;
public partial class NodeManager : Node
{
[Export]
public PackedScene NodeScene;
private readonly List<NodeScript> _nodes = new();
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled);
for (var i = 0; i < 5000; i++)
{
var node = (NodeScript)NodeScene.Instantiate();
_nodes.Add(node);
AddChild(node);
}
}
}
```
It just creates 5000 nodes and adds them as a child, each node has the following `NodeScript` script:
```cs
using Godot;
public partial class NodeScript : Node
{
private double _timer = 0;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
HandleTimer(delta);
}
private void HandleTimer(double delta)
{
if (_timer >= 1.0d)
{
_timer = 0.0d;
GD.Print("Hello");
}
_timer += delta;
}
}
```
As you can see individually each node isn't doing much, you can remove the console print if you want, it won't affect the performance in a noticeable way.
Here's the issue though, we're getting 200 FPS on a completely empty scene with no rendering happening at all on a Ryzen 9 7950X3D

On a project with more going on, I was getting around 50-40 fps, can't imagine how bad it would be on a less powerful machine.
However, if instead we remove the `_Process` method from `NodeScript`, create our own `OnProcess` method and call it inside a for loop in the `_Process` method of the script of the root node (can be any node really) the difference is pretty big:
```cs
using Godot;
using System.Collections.Generic;
public partial class NodeManager : Node
{
[Export]
public PackedScene NodeScene;
private readonly List<NodeScript> _nodes = new();
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled);
for (var i = 0; i < 5000; i++)
{
var node = (NodeScript)NodeScene.Instantiate();
_nodes.Add(node);
AddChild(node);
}
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
foreach (var node in _nodes)
{
node.OnProcess(delta);
}
}
}
```
```cs
using Godot;
public partial class NodeScript : Node
{
private double _timer = 0;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
//public override void _Process(double delta)
//{
// HandleTimer(delta);
//}
public void OnProcess(double delta)
{
HandleTimer(delta);
}
private void HandleTimer(double delta)
{
if (_timer >= 1.0d)
{
_timer = 0.0d;
GD.Print("Hello");
}
_timer += delta;
}
}
```
Our game is suddenly running at 2800 FPS

We're seeing a x14 improvement here and this isn't happening just in a synthetic test like this one, it's happening on any project with many C# scripted nodes running on the scene.
I don't know how this is internally implemented but I will guess every `_Process` function gets called from native for every node and does some kind of reflection to find either the function or the node itself, as the profiler said most of the CPU time is spent doing comparisons between `StringName` types:

93% of CPU time comes from this:

So I will guess it is really using `InvokeGodotClassMethod` to find the method by name using reflection.
Wouldn't be possible to instead make a single call to .net that just updates all nodes the same way I just did in the example?
Also please note this happens in .net 6, .net 7 and .net 8
### Minimal reproduction project (MRP)
[dotnet-performance-test.zip](https://github.com/user-attachments/files/17365904/dotnet-performance-test.zip)
2 Likes
I’ve always been interested in this, but your link is comparing GDExtensions (C++) to GDScript. In the coments they were able to optimize the GDScript from 7208ms down to 147ms, and the C++ from 66ms down to 3ms.
2 Likes
@gertkeno
That was a great link BTW, it took ages but I read most if not all of those comments. It was a great read - thanks.