godotengine / godotengine/godot

Unexpected C# performance loss on simple scripts

Open
#89,217 19 comments 36 reactions 0 assignees View on GitHub
needs testing performance topic:dotnet
Dominant language
C++
Stars
117k
Forks
26.8k
PR merge metrics
PR metrics pending

Description

### Tested versions

- 3.5.3
- 4.2.1
- 4.3-dev4
- custom build branched from 4.2 branch

### System information

Godot v4.3.dev4.mono - Windows 10.0.19045 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 2070 SUPER (NVIDIA; 31.0.15.4633) - AMD Ryzen 9 3900X 12-Core Processor (24 Threads)

### Issue description

### Short Version

C# performance seems to have suffer due to StringName equality checks in generated methods like `HasGodotClassMethod `and `InvokeGodotClassMethod`

### Long Version

I noticed that performance in my game drops sharply with a large number of nodes doing basically nothing. So I created a small repro project to try and explain what I have observed.

In the repro project we have a very simple script that toggles a Label from `a` to `b` every second. This is done in a `_process` callback in both GDScript and C#. We instantiate 1000 instances of the label to simulate a large number of nodes. At a too large number (10000) the project is very slow, so differences between GDScript and C# are not noticable.

( all screenshots are in the repro project)
Here are the graphs for the 4.x version of Godot:
GDScript
![label_gd_4 3](https://github.com/godotengine/godot/assets/2500134/3d498d98-efc1-4ab2-9913-bf20f3c0aef6)
C#
![label_mono_4 3](https://github.com/godotengine/godot/assets/2500134/14a9e8d9-a09b-4205-9413-b9eac1a33fe0)

All 4.x versions tested exhibit the same behavior, which is GDScript being almost double the speed of C#.

For comparison here are the graphs for 3.5.3, where C# is faster than GDScript:
GDScript
![label_gd_3 5 3](https://github.com/godotengine/godot/assets/2500134/19441c3c-d916-4f06-add4-ea79cea9b13b)
C#
![label_mono_3 5 3](https://github.com/godotengine/godot/assets/2500134/b3be0155-6937-42a4-9626-73327a343220)

I ran a dotTrace using Rider to try and figure out what is going on, and here are the results for 4.2.1
![test_cache_4_2_1_trace](https://github.com/godotengine/godot/assets/2500134/acbcb2d8-e8db-43b7-9b5d-59f42ec5b8dc)

it's here I first noticed the equality checks on StringNames taking up a bit of time. Which led me to assume that the if-else chain in the `HasGodotClassMethod `and `InvokeGodotClassMethod` might be part of the problem.

I experimented in modifying the ScriptGenerators to use cached method calls instead of comparison checks https://github.com/TheOrioli/godot/commit/c291f64568899bb17d300655aa6a57179ae91cb0 and it seems to provide a bit of a speedup in my main project, but I do not know how to change those methods for built-in godot types.

You can see a small pyramid in the flame graph disappear under `TestLabel.InvokeGodotClassMethod`
![test_cache_custom_trace](https://github.com/godotengine/godot/assets/2500134/a6d22a8f-dd84-465a-a952-fcbe2b5520d0)

I there is definitely something here, and there is potential to eliminate all those comparison checks which should provide a speedup and bring C# closer to GDScript in performance. I am hoping someone more familiar with the codebase can help out in figuring this out and testing more properly.

This behavior unfortunately means that making lots of small scripts in C# will lead to unintended performance degradation in games.

### Steps to reproduce

- Open repro project
- Run `main.tscn` with the Node property "Use CSharp" set to false
- Observe FPS monitors
- Run `main.tscn` with the Node property "Use CSharp" set to true
- Observe FPS monitors

### Minimal reproduction project (MRP)

4.3-dev4, contains screenshots from above in folder [test_mono_method_cache.zip](https://github.com/godotengine/godot/files/14511773/test_mono_method_cache.zip)
3.5.3 [test_mono_method_gd3.zip](https://github.com/godotengine/godot/files/14511792/test_mono_method_gd3.zip)

Contributor guide

Open the contributing guide

Research direction

Run the linked repro project and compare the FPS monitors with Use CSharp enabled and disabled. Start by examining the ScriptGenerators output around HasGodotClassMethod and InvokeGodotClassMethod, then profile the repro to confirm the StringName equality checks. Done means identifying and validating a performance improvement for generated and built-in Godot types.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
game-dev, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.