Open warappa opened 3 months ago
Does it happen if you remove the base._Process
? I don't think it's needed here as _Process
is handled by the engine itself and doesn't rely on virtual methods (none of the code templates or official demos use the parent call)
Yes, even with just a completely empty method it has this performance issues.
public partial class SimpleCube : MeshInstance3D
{
public override void _Process(double delta)
{
}
}
I investigated a similar problem for the issue #89217
The problem seems the way the engine calls the C# methods. There are a lot of string comparisons to check which method to call in the generated C# partial class.
Example:
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool InvokeGodotClassMethod(in godot_string_name method, NativeVariantPtrArgs args, out godot_variant ret)
{
if (method == MethodName._Process && args.Count == 1) {
_Process(global::Godot.NativeInterop.VariantUtils.ConvertTo<double>(args[0]));
ret = default;
return true;
}
return base.InvokeGodotClassMethod(method, args, out ret);
}
I did not create a PR because it needs quite a lot of modifications to execute calls from delegates or pointer functions but I made it work. I still need to do some testing and adjustments. I'll continue to work on it and keep you updated.
Has anyone found any good workarounds to this in the meantime? At the moment C# seems unusable if you want to have over 100 nodes with scripts in a given scene (which is likely over half of the games using C# in the first place). My current workaround is to substitute an Enemy node, with an Enemy.cs script with:
Doing this I was able to go from 120 enemies on a scene to about 500 enemies on screen without multi-threading. Although this works performance-wise, it interfaces very poorly with existing Godot APIs and I'm very quickly accruing technical debt. I might be overblowing it, but this seems like a blocking issue for most C# projects.
I'm surprised you have problems with only 50 CharacterBody3D. Usually, even a thousand of nodes and even in Debug, C# is still fast. Not as fast as GDScript on empty objects, but still very fast. You can see some more benchmarks here: #89217. Maybe there's something else that is wrong. Are you able to upload an minimum reproducible project? I suggest you create an new issue with more details and such.
I have not been able to repro the issue in a fresh project, the most I've been able to check is that performance decreases with the number of functions in a file. If I'm just spawning Node3Ds, with just 1 PhysicsProcess function I saturate at 16k nodes, with about 20 empty functions added, I saturate at 8k. This is not even close to the numbers I saw when profiling my project. I'll try reworking the code in my current project and will check if performance drops. If that's the case I will create a new issue to report my findings.
Sorry about cluttering the thread with this and thanks for the response, Hilderin.
You have a really good point about the number of methods in a C# script. That will affect the performance drastically with the current implementation. Each time Godot Engine tries to call a method to your C# script, it iterates on all method name in your script. That was what I was working for to optimize a couple months back but never finish it. I'll try to take another look.
Tested versions
System information
Windows 11 - Godot 4.3-beta1
Issue description
When instantiating a simple cube mesh 10000-20000 times, I noticed a difference in performance between "Starting from Editor" and "Starting Exported Binary".
Initially, both execution variants are performant. Then I add a C# script and the performance stays the same. But then I add an empty(!)
_Process
method and the performance tanks.Steps to reproduce
Setup Project
Node3D
CsgBox3D
nodeCSGToMesh
CsgBox3D
and rename the mesh toSimpleCube
SimpleCube
simple_cube.tscn
Instantiate 10000 Times - Good Performance
_Ready
method create 20000 instancesAdd Empty
_Process
Method - Bad PerformanceSimpleCube.cs
to have an empty_Process
methodProcess Time
Export a Release - Good Performance
You can add something like a counter to
_Process
just to ensure it is not optimized away by the compiler, eg.Minimal reproduction project (MRP)
SlowProcessRepo.zip