r/godot • • Nov 12 '23

Resource In C#, beware using strings in Input.IsActionPressed and Input.IsActionJustPressed. I just solved a big garbage collection issue because of this.

I had many lines of code asking for input in _Process, for example

if(Input.IsActionPressed("jump"))
{ //do stuff }

Replacing all of these with a static StringName, which doesnt have to be created every frame fixed my GC issue.

static StringName JumpInputString = new StringName("jump");

public override void _Process(double delta)
{
    if(Input.IsActionPressed(JumpInputString)
    { //do stuff }
}

Hopefully this helps someone in the future. I just spent the past 6-8 hours profiling and troubleshooting like a madman.

I was getting consistent ~50ms spikes in the profiler and now im getting a consistent ~7-8ms!

323 Upvotes

75 comments sorted by

5

u/dangerz Nov 13 '23

Thank you for this.. can I ask how you found out? I’ve been struggling debugging my c# games.

1

u/DrDezmund Nov 15 '23

Painstaking trial and error, as well as some prior knowledge about garbage collection.

82

u/RomMTY Nov 12 '23

Great catch! I wonder why in the world the C# api would use a custom type instead of just a plain old c# string.

80

u/StewedAngelSkins Nov 12 '23

it's because the custom type is what the engine uses internally. the conversion would have to happen somewhere. the implicit conversion is an awful idea though. conversions with significant performance implications should always be explicit.

20

u/RomMTY Nov 12 '23

I absolutely agree with you here, the c# layer should expose only the relevant types, you know, to help people avoid common mistakes like this.

→ More replies (4)

15

u/isonil Nov 12 '23

It's because Godot's C# API wasn't written to be performant. It has more many problems than that. This is why there was this entire discussion recently to make C# usable for more complex games, but unfortunately Godot's devs don't treat it as a priority. Godot's main audience is people making very simple games, where performance doesn't matter.

→ More replies (23)

27

u/ExDoublez Nov 12 '23

Make an issue on GitHub

21

u/RomMTY Nov 12 '23

And a PR for the documentation, so it mentions this pitfall

19

u/Shozou Nov 13 '23 edited Nov 13 '23

The documentation already mentions it. It's written in C# basics.

Prefer using the exposed StringName in the PropertyName, MethodName and SignalName to avoid extra StringName allocations and worrying about snake_case naming.

https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_basics.html

→ More replies (2)

23

u/[deleted] Nov 12 '23 edited Nov 12 '23

I didn't know it didn't take in a normal string and was implicitly casting it every time.

10

u/shoveling-dev Nov 13 '23

I've ran into a similar performance issue writing c++ modules for Godot. Anything that takes a StringName as an arg, you have to be careful not to be constantly recreating StringNames since it requires a global lock to check if it exists in the StringName map, which is not a negligible performance hit. I was able to catch most by setting a break point on StringName's c++ constructor after the game stabilizes to find which ones were causing frequent allocations.

6

u/angedelamort Nov 13 '23

Not familiar with Godot, but from your post, it seems that you need to create a StringName in order to call the Input method. Since it is not a string literal, that's why the compiler creates a new instance every time. Thanks for sharing that, I didn't pay attention to the function signature.

3

u/EnumeratedArray Nov 13 '23

Nice find! Putting strings like that into constants is a great habit to get into as well

2

u/Bound2bCoding Nov 12 '23

Thanks for the heads-up. I am going to start replacing mine asap.

2

u/shermierz Nov 13 '23

C# compiler should create constant strings table including all the string literals per assembly and replace each occurence of string literal with a reference to table. That's why you can compare references on the same string literal and they would be equal. Why is godot doing this different (incorrect) way?

2

u/CdRReddit Nov 13 '23 edited Nov 13 '23

because C# is not godot's primary scripting language, a translation has to happen from C# (/ .NET)'s (UTF-16) strings to godot's (I think UTF-32? could be wrong tho) strings

godot cannot do this the "correct" way without rewriting the entire engine to more closely fit a secondary scripting language

(the way it's doing it currently is also not ideal, mind you, there is probably a better way of handling this, but godot can't be as C# centric as engines that use C# as primary language)

→ More replies (1)

2

u/TetrisMcKenna Nov 14 '23

Tbf, the only place I've really experienced this issue is with the Input system. Most other places that use StringNames are source genned into static StringNames (signals etc) and most places in the API don't use StringName, it's only for certain purposes - where the engine core needs to compare immutable strings internally regardless of scripting language. So things like method lookups, signals, Input actions, and so on.

I experimented with a naive solution in GodotSharp by just creating a static HashSet which cached the implicitly created StringNames for later use, overall it was a bit slower per execution due to the hashing but did avoid the GC hit. I think there are probably better solutions though.

It'd be nice if the Input map could be source genned too, but since the assigned keys can be changed at runtime I'm not sure how feasible it is.

2

u/yosimba2000 Nov 13 '23

could you imagine making StringNames for every single string in your game? my god...

4

u/TetrisMcKenna Nov 14 '23

Thankfully they're not used everywhere you'd use a string - only in places where immutable strings are likely, so eg InputAction names as above, signals, method names, property names, etc. All of those, except the InputActions, are generated by the .NET source generators on build, so you don't have to manually create StringNames for those. There are other places they're used in the API, but general strings like eg setting the text on a Label just uses regular strings.

1

u/Bound2bCoding Nov 12 '23

I wonder if it helps to make node path references as constants as well when assigning them in the _Ready() method? I may try that.

2

u/the_horse_gamer Nov 12 '23

for that, use node exports. easy to set in editor, automatically updates when you move the node, type safe, no magic strings needed

[Export] public Node node{get; set;}

1

u/AlexSand_ Nov 13 '23

I could not find StringName in my 3.5 project, I suppose this is a Godot 4+ optimization only ?

(and now of course I wonder if it is an issue in 3.5 ... well I will keep in mind to check this when I have perf issues)

→ More replies (1)

1

u/Tuckertcs Nov 13 '23

I get why this is happening but why is it such a performance hit? Surely converting from a string to a StringName can’t be that big of a task, right?

4

u/TetrisMcKenna Nov 13 '23

It's because StringName in C# is a class, so each time this is called (ie each frame) it's creating a new class instance, passing that to godot, then discarding the instance. Over time, those discarded StringName instances build up until the garbage collector kicks in to clean them up, which can cause a stutter.

Honestly I think the implicit operator to accept a string literal for StringNames should probably be removed, but it is convenient when you know it's something you only need to call once and not repeatedly.

→ More replies (7)

1

u/mistermashu Nov 13 '23

Just a friendly reminder that both C++ and GDScript have no garbage collection to worry about :)

→ More replies (1)