godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.11k stars 69 forks source link

Expose a zero allocation API to Godot C# bindings #7842

Open reduz opened 11 months ago

reduz commented 11 months ago

Describe the project you are working on

Godot C# bindings

Describe the problem or limitation you are having in your project

For the past weeks, I've been discussing with several Unity users intending to move to Godot C# regarding dealing with the C# garbage collector.

The most common complaint I hear from users is that, in Unity, allocations can trigger unexpected GC spikes into the game.

In Godot, we target to make all of the high performance APIs (those that intended to be called every frame) not allocate any memory, so theoretically the GC should not be a problem. Additionally, Godot starting from 4.0, uses the Microsoft CoreCLR version of .net, which also supposedly has a better garbage collector than Unity.

But in all, after several discussions with Unity users, neither is enough reassurance for them, and they would really feel safer if Godot exposed a zero allocation API.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

The idea of this proposal is that Godot exposes zero allocation versions of many functions in the C# API, that users can use if they desire.

Technically, this could be done from the binding generator itself, without breaking compatibility, and without doing any modification to Godot itself.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

WARNING I am not familiar with C#, so take this as pseudocode.

Imagine you have two functions exposed as to C#:

void MyClass.SetArray( Vector2[] array);
Vector2[] MyClass.GetArray();

This works and is pretty and intuitive. However, it has two problems:

The idea is to add NoAlloc versions, which can be generated directly by the binder automatically when required:

void MyClass.SetArrayNoAlloc( Godot.Collections.PackedVector2Array array);
void MyClass.GetArrayNoAlloc(ref Godot.Collections.PackedVector2Array ret_vec2_array);

This way, we solve two problems:

As a result, the Godot API can be used entirely from C# in a way so, if this area is performance critical somehow, we ensure no memory copies, safe native access to Godot internal data and ability to control what goes to the GC.

The idea is to provide these specialized call syntaxes for functions using packed arrays, arrays and dictionaries.

If this enhancement will not be used often, can it be worked around with a few lines of script?

N/A

Is there a reason why this should be core and not an add-on in the asset library?

N/A

xzbobzx commented 11 months ago

c# is not here because it's the final ultimate tool that nothing can beat, it is here because Unity.

What point are you trying to make with this, though?

AwayB commented 11 months ago

c# is not here because it's the final ultimate tool that nothing can beat, it is here because Unity.

What point are you trying to make with this, though?

His point, very clearly, is that C# isn't the be all end all solution to gamedev. I'll add that @tannergooding's point was also that "noAlloc ever" isn't the be all end all solution to memory management.

Both these points being ignored is yet another example of what's been going on in social media in the past 2 weeks. Anything that anyone says is ignored or denied, unless it goes down the way of enforcing Unity-like designs and principles into Godot.

I can only speak for myself, but I think a lot of the Godot community would appreciate if the newcomers stopped trying to fit Godot into a Unity-shaped box and pretend that anything that may not fit that goal is ill-conceived or best left ignored. Godot is Godot. It's not going to turn into Unity. Please consider the engine's design, which I explained at length above, rather than pretending that it's not worth your time until it becomes Unity 2.

Especially when Godot is truly brilliant and deserves to be judged on its own merit and not with Unity as a metric.

AwayB commented 11 months ago

@AwayB

Is this proposal made in the interest of Godot? Is it following the proper division of labor and design principles of Godot? I think not.

Actually, I want to second this statement, despite several negative emotions that some people put underneath that post and the obvious prejudice against Unity newcomers (among whom I consider myself to be, since my previous engine was Unity, ignoring the fact that I used it only for rendering).

I'd also like to say that I have no prejudice against Unity users for coming from Unity, I fully welcome them. Many people have come from Unity and are trying to fully embrace and appreciate Godot for what it is, and I'm extremely happy to see it.

I may have no prejudice, but I do intend to make the case that Godot is an engine designed with purpose. I took a very long time to write what made Godot's design, particularly its division of labor between languages, unique and brilliant. I find it sad that this element of Godot goes ignored, and that some newcomers want to apply a policy of Unity-like designs, which I believe to be lesser. Defending what works is all I mean to do here. If anyone has a point to make against Godot's design or for Unity's design, I'd be glad to read it. I'm just not seeing one.

xzbobzx commented 11 months ago

So this proposal, which:

Technically, this could be done from the binding generator itself, without breaking compatibility, and without doing any modification to Godot itself.

...would not be detrimental to Godot's design in any way and simple exist alongside it. Which would:

The only limitation is that the Godot collections need to be exposed better and give access to things like Span, but as far as providing those functions as mentioned in the OP, an hour of work at best.

...take about an hour to implement. Yet would:

The idea of this proposal is that Godot exposes zero allocation versions of many functions in the C# API, that users can use if they desire.

...give people more freedom in how to develop their game.

Is bad because it goes against the "purpose of Godot"?

While mind you, GC in Unity was absolutely horrific and everyone specifically wants Godot to not run into issues that they experienced with Unity. These concerns stem from bad experiences with Unity that people want to move away from.

What "purpose of Godot" would disqualify a bunch of extra non-allocation API calls from being exposed? Especially if it barely takes any effort. Especially if it doesn't break compatibility or modify Godot in any way?

tkgalk commented 11 months ago

No one wants to turn Godot into Unity, don't force words into people's mouths or add your assumptions and fears of conspiracies where there is none.

No one wants this to be C# specific nor no one wants to remove GDScript or other languages that might want to use this new noalloc API.

Writing comments like this fosters disagreement, causes unnecessary division and conflict and is just plain unproductive by derailing the conversation.

Be better.

PavelCibulka commented 11 months ago

@AwayB If you know, that people are not motivated by making godot new unity. Would you change your opinion of this suggestions / improvements? Unity API is very inefficient too. They have added Span<> to few places and promised more with each version. Then they have forgot it. Unity is pretty bad in many places. No one wants another unity

Frontrider commented 11 months ago

What point are you trying to make with this, though?

A bit of a reinforcement for reduz's take, that the Godot API should not pick up anything specifically for C#. It should remain agnostic to any bindings, and as it was stated GDScript does not contradict it.

His point, very clearly, is that C# isn't the be all end all solution to gamedev. Both these points being ignored is yet another example of what's been going on in social media in the past 2 weeks. Anything that anyone says is ignored or denied, unless it goes down the way of enforcing Unity-like designs and principles into Godot.

Thank you. :)

I specifically pull the jvm up, not because of CedNaru, but because I see some movements in the java/kotlin gamedev front (triggered by Unity) and Godot is actually on the table for them over things like libgdx. (I don't know c++, so I can't help with the binding, only get some bug reports back when it is ready) It is there, because this would be the only solution that lets you use jvm languages while having an actual editor for the engine. You can get the so called "Unity like experience" that they really want.

The API should absolutely NOT do anything that is C# specific (imagine forcing Rust, or even C++ itself to conform to C# specific things), that would be a significant determent to the engine. IF you can generate it into the C# binding then that is the perfect solution, keeps the things specific to that language inside the binding.

While mind you, GC in Unity was absolutely horrific and everyone specifically wants Godot to not run into issues that they experienced with Unity.

If those issues were coming from Mono then it is already fixed by not using it. At this point it looks like that Unity needs ECS to be on pair with Godot's C# performance. We need more benchmarks for it, but that is what seems to be the case. (which again pretty much makes the question null and void without changing anything)

I'm gonna return to only reading this thread.

AwayB commented 11 months ago

So this proposal, which: ...would not be detrimental to Godot's design in any way and simple exist alongside it. Which would:

...take about an hour to implement. Yet would:

...give people more freedom in how to develop their game. Is bad because it goes against the "purpose of Godot"?

Godot is designed to consider the actual needs of game development above all.

Let me quote an excerpt of myself:

As I said above, there are 3 languages that Godot lets you develop with, and each responds to a different kind of need in gamedev: Highly tweakable components (player characters, gameplay feel, special effects, enemies, AI, immediate environment...) Regular, strong performance components (systems, game background logic...) Mandatory, best performance components (render/physics, very large amount of entities, high perf systems...)

Godot allows the easiest route for each of the needs that I've cited. Obviously, I'm referencing GDscript, C#, and C++. In refusing to use the right tool for the purpose, which in the case of a noAlloc/manual memory management policy, would be C++, the demand is placed to have C# repeat that pattern, when there is a fitting tool right there. This demand isn't formidably hard, sure. However, it is doubling the ways to do one thing, which is always a bad idea.

Encouraging repetition of patterns that fit C++ into C#, which is only requested because of the Unity users who refuse to look outside of C#, is a bad precedent. It means that we start considering repeating code, copying patterns, redoing the same work, however little work that is. Software design wise, it's generally not a good idea.

Also, the bigger problem I have, which isn't with the technical difficulty, is that it bloats the C# part of Godot with an element, which, by nature, belongs in the C++ base engine layer. Having a C++ engine and asking for its C# layer to cover the ground of a noGC policy is well...like putting a pizza on top of a pizza for that extra layer of crust. It's bad by software design standards, bad by leanness, bad because it's repetitive, it's pretty much bad all over.

Also, and I'm sorry to invoke you again for this @tannergooding , but from what I gather of your long post, which is something I've thought for a long time, memory management is fundamentally difficult and full of little subtleties that are best left untouched unless you seek the absolute highest tier of performance possible. This "top tier perf"...is something that in my opinion absolutely does not belong in the C# layer, which is meant to serve people with a clean API that opens a general purpose programming language and its libraries to everyone.

If you want to tango with CPU optimisation, SIMD, memory latency tests, and DB/system level API tiers of performance, by all means, I'll love to see it. But not in the general purpose programming part of the engine that's meant to be welcoming to any and all game dev. Do it in C++, with the real nerds :)

While mind you, GC in Unity was absolutely horrific and everyone specifically wants Godot to not run into issues that they experienced with Unity.

At the risk of repeating myself for the 3rd time, you are not at Unity's here. You're outright stating that you're projecting the problems you had there here, without having even bothered to ask if the same problems will happen again. From what I've gathered, they won't.

What "purpose of Godot" would disqualify a bunch of extra non-allocation API calls from being exposed? Especially if it barely takes any effort. Especially if it doesn't break compatibility or modify Godot in any way?

I've already answered that question broadly, so let me just quote one of the greatest quotes ever made:

"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away." - Antoine de Saint Exupéry

Godot's lightness is taken as a "nice thing". It's not. It's a testament to how rock solid its design is. Adding bloat is walking away from what's possibly its most amazing trait.

wareya commented 11 months ago

That's the thing. This isn't bloat. This is a straightforward change with a very limited impact on Godot's codebase, and it could benefit interop with almost any language with a GC, not just C#.

AwayB commented 11 months ago

That's the thing. This isn't bloat. This is a straightforward change with a very limited impact on Godot's codebase, and it could benefit interop with almost any language with a GC, not just C#.

Please explain to me how an engine whose core language has manual memory management, and a higher level language incorporated, can rewrite the same exact same memory management pattern in the higher level language without it being repetition, and thus bloat.

@AwayB If you know, that people are not motivated by making godot new unity. Would you change your opinion of this suggestions / improvements? Unity API is very inefficient too. They have added Span<> to few places and promised more with each version. Then they have forgot it. Unity is pretty bad in many places. No one wants another unity

Haven't touched Unity in so long that I forgot the year I touched it, sorry. And even if I knew Unity like the back of my hand, I'd rather Godot stay designed on Godot principles, and ignore Unity entirely. I think Godot's design is superior anyway.

tannergooding commented 11 months ago

@reduz, Just wanting to make sure I understand.

Using Span in Godot arrays

Here, you're covering that it should be possible for Godot.Collections.Array to itself expose a way to get a Span<T> directly to this underlying buffer on the C# side. This would allow it to be used in more places across the BCL, to avoid copying, allow bounds checks to be elided where the JIT support exists, etc.

It would not allow other Godot APIs to take that Span<T> however. Only provide a way to more easily consume the Godot collections from non Godot APIs.

Is that right?

-- If so, I think this is general goodness. Being able to take a Godot.Collections.Array and use it with the rest of the .NET APIs in the BCL that take Span<T> just makes everything better. It also allows Godot developers choosing C# to take advantage of the already highly optimized algorithms the BCL provides for many scenarios.

I explained this a couple of times, but I guess too many messages here to read for everyone. Godot uses its own native collections (refcounted arrays) in the majority of the API. As such it is not possible for Godot to provide an API based on Span to C# under any circumstance when on the Godot API side you have an array. Please understand this, It's not technically doable in any way, stop asking for it.

Here, you're effectively saying that at the Godot API level, you have APIs that might look like void Method(Godot.Collections.Array input); (pseudo-code). Where Godot.Collections.Array is effectively a wrapper over a pointer (to the backing allocation) and a tracked ref count so that it can be cleaned up when nothing references it anymore?

Because of this, exposing a C# binding for such an API that looks like void Method(System.ReadOnlySpan<T> input); is not possible because the API hasn't been designed to work with memory like this.

Is that about right?

-- If so, this is understandable. That being said, if the requirement is just that you need to be able to track the Godot.Collections.Array so that you can do appropriate ref tracking, etc. There is are still a few viable solutions here that would allow more the existing APIs to be used while allowing C#/.NET to more readily work with no-allocation, no copying scenarios.

All of these "alternative" solutions are effectively rooted in the idea of how Memory<T> works or how "portable Span<T> works.

For those unfamiliar, Span<T> as it exists on modern .NET is a ref struct and tracks an "interior pointer". That is, it has two fields ref T address; int length;. This makes it incredibly small and efficient (address computation is just address + offset and is bounds checked against length), but the GC has to do specialized tracking so it can determine the root allocation that address refers to. This is why it is a ref struct and limited to the stack, so it is more "pay for play" and the GC only has to do range comparisons for stack based refs and not for "any reference".

Memory<T> and "portable Span<T>" (which is to say the Span<T> that exists on .NET Framework) work a bit differently and they track 3 fields instead: object root; int start; int length;. This makes it slightly larger, but allows it to keep track of the original object without the GC needing to do range calculations. Address computation is then root + start + offset and bounds checking remains validated against length. This makes it slightly more expensive to pass around, but basically as efficient to actually use for most loops.

"If" there were scenarios where accessing or performing an operation on a slice of memory was beneficial, Godot could do something similar to the latter for its collection types. Which is to say, the underlying API could be extended or have an alternative provided that looks like void Method(Godot.Collections.Array input, int offset); or void Method(Godot.Collections.Span input); (where Godot.Collections.Span is itself basically Array array, int offset).

Such an approach would allows users to use a subset of their buffer; without removing Godot's ability to do correct tracking and lifetime management. It would also not significantly change the code to add such support, as the existing void Method(Godot.Collections.Array input) API, simply defers to Method(input, offset: 0).

wareya commented 11 months ago

@AwayB

Please explain to me how an engine whose core language has manual memory management, and has a higher level language incorporated, can rewrite the same exact same memory management pattern in the higher level language without it being repetition, and thus bloat.

See the OP:

Technically, this could be done from the binding generator itself, without breaking compatibility, and without doing any modification to Godot itself.

The code for this can be mechanically generated and it would not take up very much space at all.

To answer your question of "how", the current problem is allowing Godot objects to theoretically create GC pressure by creating a new one on the managed side every time the given function is called. This proposal allows in its implementation for objects to be reused on the managed side without affecting how the native side is implemented.

If this is implemented as copying newly-created objects that only native ever sees into objects that the managed side never has to reallocate, then you've successfully avoided creating new GC pressure. Copying is not the optimal way to implement this, but it is indeed not bloat, just a couple line of code per wrapped function. If this proposal turns out to be useful, then a more optimal version that doesn't perform extra copies can be implemented. If it's not useful, then it can keep the copy-based implementation.

tannergooding commented 11 months ago

-- Just to note that, of course, the "if" is the most important part there on the latter half of the comment.

As already indicated by others in the thread, not all APIs need this support nor would it be beneficial to provide everywhere.

But, it does represent a path that could be considered and which benefits all languages (including, at least conceivably, Java, C++, GDScript, C#, etc) and which would not represent significant effort to enable when and where cases were found that such support would be beneficial (at least after any foundational support to support the pattern or new type was defined).

Interested in hearing back and discussing it further if you're interested

ProTip commented 11 months ago

I was confused as well and thought perhaps there is a conflation between C++ span and C# Span<T>(which does not need a C++ span to work with).

Poking through the code it seems like PackedVector2Array is just a Vector<Vector2> :

https://github.com/godotengine/godot/blob/df0a822323a79e1a645f0c6a17d51c7602f23166/core/variant/variant.h#L74 typedef Vector<Vector2> PackedVector2Array;

The "Packed" just differentiates these from the internal variant array type. And also internally there is the PackedArrayRef<T> which wraps Vector<T> and adds the ref count: https://github.com/godotengine/godot/blob/df0a822323a79e1a645f0c6a17d51c7602f23166/core/variant/variant.h#L207

It sounds like GDExtension will expose and take the Packed*Array types which are just Vector<T> though?

AwayB commented 11 months ago

Please explain to me how an engine whose core language has manual memory management, and has a higher level language incorporated, can rewrite the same exact same memory management pattern in the higher level language without it being repetition, and thus bloat.

To answer your question of "how", the current problem is allowing Godot objects to theoretically create GC pressure by creating a new one on the managed side every time the given function is called. This proposal allows in its implementation for objects to be reused on the managed side without affecting how the native side is implemented. If this is implemented as copying newly-created objects that only native ever sees into objects that the managed side never has to reallocate, then you've successfully avoided creating new GC pressure. Copying is not the optimal way to implement this, but it is indeed not bloat, just a couple line of code per wrapped function. If this proposal turns out to be useful, then a more optimal version that doesn't perform extra copies can be implemented. If it's not useful, then it can keep the copy-based implementation.

The actual answer to the question was "It's not possible, since it's rewriting the same workflow with custom code, in a language that's not meant for it.". What you answered was how the bloat would work, and sure, it'll work. It's still bloat, and while all software will bloat left and right eventually, there's necessary bloat and unnecessary. I don't think this bloat is at all necessary or positive, and I think it's a terrible idea to do it under pressure from people who won't even consider how Godot is made and would rather misjudge it then push it to "Unitify".

I think I'm done with this issue, I've said all there is to be said and the only responses have been accusations (someone even went with the ever so hilarious "conspiracy theory") and denial. There is no counterargument, and thus my point stands as it did in my original post. I believe that this proposal is not to the advantage of Godot and will only welcome more pressure and demands from a vocal bunch of Unity refugees in the future. I believe the best thing to do is to explain why Godot is made the way that it is, invite them to appreciate it for what it is, and if they will not, to politely tell them that it is probably not the engine for them.

xzbobzx commented 11 months ago

Let's say this is implemented, what part of the "bloat" would actually affect any Godot user?

Would the engine become slower? Would it become harder to build? Harder to maintain?

I can't imagine any of this being the case, most regular users wouldn't even come in contact with it and on the build side the whole thing appears to be automatic.

So beyond a slippery slope argument the only other argument against its favor would be "Maybe it doesn't help as much as people think it will."

Which, okay fair, but beyond dealing in hypotheticals the only way of actually knowing if that's the case is testing the code and playing around and benchmarking it.

GabrielRavier commented 11 months ago

@AwayB So I hope I haven't misunderstood anything that you've been saying here, but so it seems to me like what you're saying boils down to the following: C# must, within the context of Godot, always remain as a language that is the middle ground between GDScript and C++ - slower iteration than GDScript, but faster than C++, and better performance than GDScript, but slower than C++.

As such, I would like to ask you one thing: Why is that fundamental to Godot's design ? It seems to me like you've simply been repeating that same thing over and over in different manners each time, that C# must always be the middle ground and not try to expand anywhere beyond that, but nowhere have I seen an explanation as to why it not trying is so important for Godot.

To me at least, it seems obvious that even if you have such views, you should still be able to support this proposal - if it would be so detrimental to Godot's fundamental design principles, then trying to put the proposal into place would make that obvious, and then it could be reverted. From what I can see, this seems like the obvious way to go, especially given the quite small technical cost of the proposal, which, from what I have seen, you have not disputed.

tkgalk commented 11 months ago

Last post from me that is off topic.

Is Godot made by its community or not? If a large part of that community, even if new, wants something done should their needs/ideas be gate-kept just based on the previous engine they used?

Food for thought.

wareya commented 11 months ago

Getting built-in C# profiling is a long-standing feature request. It is not primarily coming from recent new users.

reduz commented 11 months ago

@tannergooding

Here, you're covering that it should be possible for Godot.Collections.Array to itself expose a way to get a Span directly to this underlying buffer on the C# side.

Yes, to be more specific, Godot internally uses thread safe copy on write reference counted arrays to move buffers of data around. This works exceedingly well for the engine design because it lets you keep them, modify them, etc. without risk of breaking anything else and without doing extra copies.

This way even cool things like loading a mesh buffer from disk, and sending it to the render thread via command queue for registering there can be done without additional code, or during multi thread scene processing you can defer calls between any function in the engine using those arrays.

Godot uses this a lot and its one of the core parts of the architecture that lets it be very efficient and keep it simple.

Technically these arrays should be exposed as Godot collections, but currently the C# API does not give you very good access to them.

The ideal implementation is that they are better exposed to C#, so you can access their internals via Span. These buffers are safe, if you try to write to them they will do copy on write (if used somewhere else), otherwise if you want to just read, it does not do any copy (so you need to use a const version of the Span).

Exposing the actual API calls with those arrays, and exposing their internals via Span should solve the performance issues mentioned above.

Because of this, exposing a C# binding for such an API that looks like void Method(System.ReadOnlySpan input); is not possible because the API hasn't been designed to work with memory like this.

Yes exposing methods like this is not possible, you have to use the existing Godot collection objects in order to access their memory directly. You have to create a buffer, resize it, then access it via Span and write it and finally send it as argument. You can reuse that buffer as much as you want if you don´t want to trigger the GC.

tannergooding commented 11 months ago

Is Godot made by its community or not? If a large part of that community, even if new, wants something done should their needs/ideas be gate-kept just based on the previous engine they used?

Notably, community made doesn't mean doing anything that any member of the community wants, nor does it even necessarily catering to large portions of the community.

It is the responsibility of the maintainers, of any project (OSS or not), to take a comprehensive look at what's being asked and determine whether or not its a good fit. In an ideal world, they will get the resulting decision right most if not all of the time.

But, we aren't in an ideal world and maintainers will make mistakes. At the same time, so will the community. The reality is that a majority having a shared idea doesn't necessarily mean that idea is good or bad, it can be either or even both. No one has all the context and no one has future sight.

The Godot maintainers obviously have some idea around what they're doing, as they've successfully maintained and grown the repo for nearly 10+ years now after all.


It's very clear that a large portion of the .NET community cares deeply about perf. It's also clear that Godot does as well, but that there are some considerations that come from the Godot side around various factors that do impact the overall maintainability and usability of the codebase.

It is not as simple as just exposing new C# APIs in the ideal shape, it is not as simple as just adding APIs in the ways that people may expect. There is a lot of complexity and consideration to exposing new APIs, to supporting new patterns, and in general to extending that to all the places that may need, want, or benefit from similar functionality.

So, ideally, we in the .NET community work with the Godot maintainers to better understand what limits they have in place and why. We consider what our core needs are and how those could be achieved given any limitations or constraints the maintainers set out. We then work together to see if a reasonable compromise can be found.

That's why I initially gave some context as to how this is handled in modern .NET. I then asked for more clarity around what reduz said in response, and offered an alternative that would still allow .NET to benefit as well as other languages, in a way that (at least to my current understanding) might mesh well with the existing codebase.

It could be that I'm still missing key context, but the important bit is to have a two way discussion and really attempting to understand these core factors and limitations. With enough discussion and interest, we can surely find some common ground that works for everyone and doesn't needlessly force churn where it isn't actually required. -- If a pattern can be found and agreed upon, then it can be lit up as needed, much as we did in the BCL when adding Span<T>. We didn't get everything in .NET Core 2.1 and we still don't have "everything" in .NET 8. We've incrementally added the support to core APIs and otherwise based on user request, need, feedback, and perf data showing its required.

We expect the same communication and open discussion for API suggestions and contributions on dotnet/runtime, and so its more than reasonable to provide the same courtesy here to Godot ❤️. -- We also, have "angry" users plenty because we have to tell them "no". They don't care that we have more context and insight into how to maintain the runtime and refuse to see it anything but their way, and that's just generally not beneficial to anyone.

tannergooding commented 11 months ago

Thanks for confirming @reduz!

It sounds, then, like there are some good options possible for places that are actually identified to need it.

Much like on the other thread I'm on around improving the numerics types (godot/issues/69490), I'm more than happy to continue discussions in this area and to provide any insight into common pitfalls or other considerations for .NET

If there's anything I can do to help out in this area or to help prototype, let me know and I'd be more than happy to (I'm also on the Discord now, so am there as tannergooding as well)!

Repiteo commented 11 months ago

@AwayB

Please explain to me how an engine whose core language has manual memory management, and a higher level language incorporated, can rewrite the same exact same memory management pattern in the higher level language without it being repetition, and thus bloat

Actually, I can touch on this! One of my previous C# PRs had the intent of improving the binding generation in several respects. One such improvement was making the generated documentation significantly smaller by removing redundancies via <inheritdoc/>. However, this was ultimately decided against; because, while it made the generated code smaller, it served to make the actual binding code itself more convoluted—which is the area where contributors will actually be working. As raulsantos put it:

In general, we should prioritize cleaner code in the non-generated code since it's the code that we have to write and maintain ourselves, the generated code can be as long and repetitive as we want since it's machine-generated.

This is to say that, despite surface-level redundancies, it's not necessarily to the benefit of the codebase to do absolutely everything to remove said redundancies. In other words, repetition is not inherently bloat. Repetition can have the benefit of making the elements of the codebase that are actually worked with much leaner and readable, without compromising functionality. Repetition can have the benefit of only showing itself in generated sections of code, which is exactly what this binding scenario would represent. Repetition stands to have nothing to lose in a scenario where, even in the hypothetical situation where it produces no real-world benefit, introducing it would have less-than-negligible performance and maintenance costs.

EgorBo commented 11 months ago
  • GDscript, VM, instant iteration, quick to write and read, highly integrated, mild performance
  • C#, compiled, slower iteration, access to a massive ecosystem of libs, great performance

My 2 cents on this - why is it an issue for C# to be slower for the inner developer-loop. C# has an interpreter and hot-reload abilities. Roslyn is also extremely fast with the compiler server.

tkgalk commented 11 months ago

I'm also not sure why, but it does feel like C# "has to be relegated to second-class citizen or people get angry/overly protective".

Even when I said that C# profiler would be a great feature that was somehow taken as "inflammatory".

Making one thing better doesn't mean making your chosen language/engine worse. It's not a zero sum game.

rodrigofbm commented 11 months ago

Getting built-in C# profiling is a long-standing feature request. It is not primarily coming from recent new users.

I'm using C# since it's production ready in 3.x. I think this guy has nothing batter to do other than shouting things from his imagination.

reduz commented 11 months ago

Folks please chill. Godot is driven by community demand. If significant community wants to implement C# and they want to maintain it, then nobody is to oppose this.

If significant amount of the community wants to implement JavaScript and they want to maintain it its the same, nobody will oppose (not the case though :sweat_smile: )

There are no resources wasted since those who want to work on something will do, and those who don't will not. This is the beauty of FOSS.

wareya commented 11 months ago

Hey, Javascript is actually a really good idea, since it's the only way to get JIT on iOS!

Visne commented 11 months ago

They said they were done with this issue, let's stop pinging them and get back on topic.

nockawa commented 11 months ago

This thread has a lot of off-topics and discussion, so it's hard to follow things and not posting doing the same, but I'll try to contribute anyway, hoping I won't waste your time and/or add noise.

Wrapping a native memory buffer (allocated by something else than .net) in a very efficient way in .net is fairly easy. You can create a Span<T> instance only with an address and a length. You just have to ensure this memory block has a lifetime as big as the Span you create. If someone a reallocation occurred, then .net is like C++, it will crash unexpectedly.

So if you have C++ exposed API to:

Then it's fairly easy to expose the array in .net, expose (ReadOnly)Span<T> access, implement Enumerator (using ref struct to avoid GC alloc of the enumerator) to allow foreach and even IEnumerable<T> to allow LINQ. Performances will be nearly identical with C++ and your Array will be compatible with many .net APIs working on data manipulation.

Overall, you must avoid:

thread safe copy on write reference counted arrays to move buffers of data around. Could you elaborate please?

It means data is shared as long as it's accessed in read-only, but if you want to modify the content then it creates your own copy? Is there online documentation about this I could read? Edit: found it.

r-bertolini commented 11 months ago

@Visne fair enough, I've deleted my previous post.

minim271 commented 11 months ago

Really glad to see this proposal, maybe i can close #2757 ?

ewrogers commented 11 months ago
void NoAlloc.MyClass.SetArray( Godot.Collections.PackedVector2Array array);
void NoAlloc.MyClass.GetArray(ref Godot.Collections.PackedVector2Array ret_vec2_array);

Personally I don't find this namespace prefixing intuitive for several reasons:

  1. I need to be aware of the namespace
  2. It won't show in auto-complete side-by-side in IDEs (SetArray() + SetArrayNonAlloc())
  3. Refactoring becomes a lot harder to find/replace (s/SetArray/SetArrayNonAlloc/g)
  4. Going against convention, which was sparked by Unity developers in the first place

The signatures for *NonAlloc() variants will usually be different anyhow, so I don't see how the namespace part really adds any value compared to a different method name.

The main benefit of the non-alloc versions is the ability to re-use buffers so their signature is typically "same but you pass in the array/list storage" instead. Typically in raycasts.

makemefeelgr8 commented 11 months ago

Godot exposes zero allocation versions of many functions in the C# API, that users can use if they desire. I am not familiar with C#

But you're trying to optimize GC calls?! Oh man... Well, I see what happened here. Let me help you. There's the thing called "P/Invoke". I'm begging you: just read about it. Please. Here, I'll even leave the link: https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke This is industry standard. This is how you make C# work with native code. This is the way.

When you're done reading, take a look at this one too, as it will allow to compile C# runtime into a tiny binary (and facilitate porting to mobile devices, once dotnet8 is out): https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot

As for the GC- don't worry about it. C# integration has bigger issues now. WAY bigger. Experienced C# programmers can easily deal with memory allocations and write performant code, GC or no GC.


Why are you booing me? I'm right!

Seriously, though, I'm back with some numbes, and I've even got some code to prove my point. This is how one can force the GC to perform the absolute worst nightmare ultimate type of garbage collection, and completely halt the application until it completes! This is the worst scenario that never happens in real life. But it makes for a nice test. It doesn't get worse than this.

GC.Collect(2, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();

I challenge you: put this into a _Process function, of your project, and test how much performance you loose! It was about 15% for me.

...and now let's take a look at the infamous article about the overhead of calling raycasting APIs and compare the numbers. This is where you loose not 15%, but 1500%.

So, if I were to compose the list of proirities of what should be done to make C# integration work better, I'd start with addressing the elephant in the room, and leave the GC alone.

SamPruden commented 11 months ago

I'd like to clear up some confusion that's happened (and that I might be a little responsible for!) over the argument between passing in destination buffers and returning Godot arrays. These are both things which we would want to do under different circumstances! There's no either-or or contention between the patterns.

Returning arrays from core

As @reduz has pointed out, Godot's packed arrays can be very efficient in certain cases because they're implemented with CoW, which allows them to be passed from the core into user code with no copying or allocations (except the wrapper, in C#'s case). These are great in circumstances where the core is already holding such an array preallocated internally. Semi-random examples of this include MeshDataTool::get_vertex_weights, HeightMapShape3D::get_map_data, Curve2D::get_baked_points, and CPUParticles2D::get_emission_points. These are the perfect tool for the job in this situation.

However there's a second category of method which in my analysis so far is actually more common, and is certainly very common. These are methods which allocate and write into a brand new array for the purpose of returning it. In a few cases this is directly copying data from other places which could possibly be promoted to use CoW, but in many cases this is done because the core simply does not and cannot cache that information, typically because it depends on parameters. Examples of this include AStar2D::get_point_path, Area2D::get_overlapping_bodies, Curve3D::tessellate, Geometry2D::intersect_polygons, and PhysicsDirectSpaceState2D::intersect_point.

It's this latter category which would benefit from the user being able to supply a target buffer. This allows the user to skip heap allocations by writing into pre-allocated buffers, to take advantage of a bump allocator (Rust would probably love this! And C# too.), and to put data into collection types which are more ergonomic to use in that language, not to mention sometimes faster because they never have to call back into core for any operations. [Edit: Calling back into the core may be avoidable in more cases anyway.]

Passing arrays into core

When passing data from script into core APIs, it's advantageous to be as flexible as possible (up to a point) about what format that data comes in in. This helps the various language bindings with their various collection types and patterns to efficiently pass data into the core without having to do copies in script in order to get it into a useful format, or to write less ergonomic code using Godot collections instead of the native ones.

Sometimes we want this input type to be a CoW Godot array because Godot can hold onto the data without copying, which is perfect. However, sometimes that's not practical for the user, and sometimes Godot doesn't even want the array to leave the callstack so there's no advantage to it being CoW. Ideally, API functions should be able to handle both of these cases depending on what the user gives them.

Postamble

None of this is C# specific. I may have gotten myself a little bit of a (exaggerated) reputation as a C# fanatic, but I've been thinking about this from a GDE first perspective. These ideas are as language agnostic as it's possible to be - they define patterns for basic communication protocols between the core and the scripting language, and the scripting language can interface with those protocols in whichever way works best for that language.

danbolt commented 11 months ago

It's this latter category which would benefit from the user being able to supply a target buffer. This allows the user to skip heap allocations by writing into pre-allocated buffers... and to put data into collection types which are more ergonomic to use in that language, not to mention sometimes faster because they never have to call back into core for any operations.

I think @SamPruden's suggestion here would be a really lovely addition for teams shipping a GDScript project that has certain Nodes rewritten in C++. The programmer that's doing the optimizing can take advantage of how the memory's being managed for their specific use case in their specific hierarchy.

lewiji commented 11 months ago

@makemefeelgr8

But you're trying to optimize GC calls?! Oh man... Well, I see what happened here. Let me help you. There's the thing called "P/Invoke". I'm begging you: just read about it. Please. Here, I'll even leave the link: https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke This is industry standard. This is how you make C# work with native code. This is the way.

Juan didn't write the interop code, nor will he be writing this API if it's implemented. There are C# experts who do understand this stuff doing the work.

https://github.com/godotengine/godot/pull/64089/commits/2c180f62d985194060f1a8d2070c130081177c90

3.x used P/Invoke for its interop, the CoreCLR implementation moved to pointer invocation to avoid pinning. Whether that was a good idea or not, I don't know. But the suggestion that the contributors to C# support don't know what they're doing is easily disproved in the commit history. There's a precedent of using P/Invoke, the current implementation does something different, if P/Invoke proves to be a better way of doing things, Godot can go back to that method.

When you're done reading, take a look at this one too, as it will allow to compile C# runtime into a tiny binary (and facilitate porting to mobile devices, once dotnet8 is out): https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot

https://github.com/godotengine/godot/pull/64089/commits/4b90d162502d65f20a89331898cd8a0b3eea8fe2

The contributors are already aware of this and have made an effort to make this possible from the very beginning of the CoreCLR integration.

So, if I were to compose the list of proirities of what should be done to make C# integration work better, I'd start with addressing the elephant in the room, and leave the GC alone.

I actually overall agree with your point, GC pressure has never been a huge issue for me (using a C# ECS library with godot, and having used C# with godot for several years now) and most areas where it could become a problem are easily identified in a profiling session and worked around. There are devs who are very sensitive to garbage generation, and maybe in certain projects it could become an issue, but in general, if people are that sensitive to GC they can use another language without GC. There are other proposals and ongoing PRs that are intended to address the problems in the internals around allowing for structs and structuring the return values of API methods. A proposal here isn't a statement of priority; it's used to gauge community interest, gather opinions from contributors about their concerns or technical ideas, and figure out priority from there. I think this proposal is an interesting idea, but I don't think it's a huge priority right now.

makemefeelgr8 commented 11 months ago

@lewiji Thanks for the info! I thought the guy who is not familiar with C# is going to implement performance critical piece of code, and it sounded kinda questionable at best.

3.x used P/Invoke for its interop, the CoreCLR implementation moved to pointer invocation to avoid pinning. Whether that was a good idea or not, I don't know.

And this made C# code in Godot use a lot of unsafe stuff by default. Not the best idea, if you ask me, but well... They were using some similar trickery back when SharpDX was alive, so I guess the benefits are worth it after all.

About the AOT feature. I'm talking AOT + direct P/Invoke. The thing's fast. You can even do some static linking. Totally worth checking out, especially with dotnet 8 rc.

I think this proposal is an interesting idea, but I don't think it's a huge priority right now.

Now these are great news. Instead of making tight coupling to C# and exposing some weird internal methods with no GC, I'd rather propose they provide a dll file, kinda like WinAPI does. One can generate bindings for any language (nodejs, etc) in no time, and make a nice integration. Add a nuget/npm package with some types & bindings, and C#/nodejs integration's done. Well, that depends on a general roadmap. And the built-in C# IDE they're talking so much about and making an accent on... It's kind of not a real thing. It's about as good as a notepad, without code highlighting, without autocomplete, intellisense, resharper, copilot, custom plugins. I don't see a C# programmer using it. Like, there's absolutely no way.

uzkbwza commented 2 hours ago

I have experienced the limitations of this first-hand, while working on an asset that heavily queried the physics and rendering servers (>1000 times per frame). the c# equivalent code was far, far slower than the Gdscript code I wrote first, and there was nothing I could do to improve the performance. it is not just the garbage collector that is a problem, allocating so much memory every frame is an enormous hit to performance. this proposal is nearly a year old, yet it appears almost nothing has been done about this ubiquitous and significant problem which completely alienates any serious dev who wishes to make a game with many elements in c#. please tell me I am wrong and I am missing some kind of progress on improving this situation.

It seems c# is only more performant than gdscript when your game logic does not interact with the API.

Braveo commented 1 hour ago

I have experienced the limitations of this first-hand, while working on an asset that heavily queried the physics and rendering servers (>1000 times per frame). the c# equivalent code was far, far slower than the Gdscript code I wrote first, and there was nothing I could do to improve the performance. it is not just the garbage collector that is a problem, allocating so much memory every frame is an enormous hit to performance. this proposal is nearly a year old, yet it appears almost nothing has been done about this ubiquitous and significant problem which completely alienates any serious dev who wishes to make a game with many elements in c#. please tell me I am wrong and I am missing some kind of progress on improving this situation.

It seems c# is only more performant than gdscript when your game logic does not interact with the API.

I feel like the plan to move C# to GDExtension that I've been seeing around is what's making this process take longer, as far as I know. There's also the struct type that's in the proposal stage for gdscript to manage data differently in a way that would make C# support better, though I definitely do wish there was more being discussed about this.