GodotObject vs RefCounted: useless in C#?

Godot Version

Sdk="Godot.NET.Sdk/4.4.1"

Question

I have a followup question regarding this thread: Does the GC collect GodotObject Instances?

The documentation page for RefCounted states:

//     Note: In C#, reference-counted objects will not be freed instantly after they
//     are no longer in use. Instead, garbage collection will run periodically and will
//     free reference-counted objects that are no longer in use. This means that unused
//     ones will remain in memory for a while before being removed.

The dotnet runtime already maintains a reference count for all your objects so it can run its garbage collector. If that’s all RefCounted does, then doesn’t that make it redundant in C#?

GodotObject is IDisposable so any memory cleanup that the engine requires would be implemented in Dispose() or the base finalizer when the GC does its thing. Or am I wrong?

Is there any advantage to using RefCounted over GodotObject? It seems like that makes it do the same thing twice.

It sounds like you are confusing managed and native memory. .NET only knows about its own stuff on the managed side. The native side uses RefCounted for reference counting, to free native objects when the ref count reaches 0.

The quoted note from the docs mostly exists as it might look like you are leaking memory even when you are not. RefCounted native objects are only freed after the .NET GC cleared them on the managed side.

You can force that process by calling GC.Collect() during/after a loading screen for example. That way you don’t start into a new scene with all the memory leftovers from the previous one.

It implements IDisposable. The engine should receive a signal to release its resources when that fires. There’s no need for a second reference counter.

Interesting. Does that mean RefCounted only exists to provide memory leak warnings? If that’s how it works, perhaps that’s one advantage difference.

There’s no need for a second event to trigger the engine to release resources. The whole thing should be done with IDisposable.

Also, I think you’re confused about managed vs unmanaged code. All the memory is native.

It’s not in this context, it should be obvious that native means unmanaged here. The .NET runtime has its own, managed heap memory with a GC and its own reference count.

That’s what’s already happening? The native reference counter changes when the managed instance is disposed, as it drops the strong reference to the native instance. The managed reference counter has nothing to do with this and exists only in the .NET runtime.

No, you seem to completely misunderstand why that mechanic is there. It is there for the native/unmanaged engine side, as there is no GC built into the engine on the C++ side.

You can manually call Dispose() in C# without waiting for the GC. That’s risky though, as this can prematurely free the native instance when there are other managed references still active. Only do this if you are absolutely sure that the managed instance is no longer in use anymore. Otherwise just wait for the GC and be happy that you don’t have to manage memory manually.

Native refers to compatibility. You’re confusing compatibility with memory management. All the memory on your computer is native, because it’s running on your computer. By contrast, a binary compiled for Windows isn’t compatible with Linux. Same with different chipsets that have incompatible instruction sets.

Java and dotnet compile into an intermediate language that gets JIT compiled by the runtime when the program starts. See the first paragraph on Native AOT deployment for more information.

Your discription of Dispose() seems exactly backwards. You call it to release external resources. In my testing, calling Dispose() on a Node has the same effect as calling Free(). It causes IsInstanceValid() to return false. As long as it gets called in the GodotObject finalizer, it should get cleaned up automatically.

I think the RefCounted type is there to support GDScript and other languages, hence this thread. For C#, I think you should always use GodotObject and let the dotnet GC handle things the way Microsoft optimized it.

I made this test thing. It shows that calling Dispose() seems to clean it up fine. I couldn’t force it to call the finalizer, though.

using Godot;
using System;

public partial class MemTest : Node
{
    public override void _Ready()
    {
        var obj = new GodotObject();
        GD.Print(IsInstanceValid(obj)); // true
        obj.Dispose();
        GD.Print(IsInstanceValid(obj)); // false

        var managed = new FinalSnitch();
        var id = managed.GetInstanceId();
        GD.Print(IsInstanceIdValid(id)); // true
        managed = null;
        GC.Collect();
        GD.Print(IsInstanceIdValid(id)); // true
    }
}

public partial class FinalSnitch : GodotObject
{
    ~FinalSnitch()
    {
        GD.Print("Finalizer popped on " + GetInstanceId());
    }
}

After doing some more digging, some people say finalizers aren’t reliable by default. The documentation on cleaning up unmanaged resources has a big warning that says:

Object finalization can be a complex and error-prone operation, we recommend that you use a safe handle instead of providing your own finalizer.

It says using a SafeHandle is the recommended way to make sure your Dispose() thing gets done. But again, once you have that, the GC can do all the accounting instead of having a separate RefCounted thing.

Consumers of your type can then call your IDisposable.Dispose implementation directly to free memory used by unmanaged resources. When you properly implement a Dispose method, either your safe handle’s Finalize method or your own override of the Object.Finalize method becomes a safeguard to clean up resources in the event that the Dispose method is not called.

*Actually, it looks like SafeHandle is more for OS resources. Godot already has its ID thingie internally. It should already clean up after itself, I just don’t know how to test it.

What @Armynator means with ‘native’ memory is the memory that is managed by the engine/c++ side.

When you call new GodotObject(), a ‘native’/‘engine’ object is created as well as a C# wrapper. Only the wrapper is managed by .net while the actual object is managed/owned by the engine. Likewise only the wrapper is garbage collected, not the object itself. You need to explicetly free the object for it to be released.

Yes, you are wrong here ;). Dispose only disposes the wrapper-object and removes the reference to the engine object.
Due to the latter your test seems to succeed. IsInstanceValidwill return false when the wrapper is disposed (there is no pointer to the object anymore), even though the actual object still exists in the engine.
You can verify this in the Monitor tab in the debugger in the editor. It will show the actual memory usage and object count of the running project.

1 Like

Thank you. So, as a solution to OP, does RefCounted handle the cleanup that Dispose() doesn’t take care of?

If yes, then the answer is: “Yes, RefCounted is useful in C#.”

To me, it rly seems like the engine cleanup should be triggered by a call to Dispose(). That’s what it’s supposed to do. It’s misleading to have the instance ID invalidated if the object is still hanging around in memory.

If calling Dispose() doesn’t clean up the object, it’s simple enough to add it with something like this:

using Godot;
using System;

public partial class BetterGodotObject : GodotObject
{
    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (IsQueuedForDeletion()) GD.Print("Oh look, it's already being freed!");
        else Free();
    }

    ~BetterGodotObject()
    {
        Dispose();
    }
}

(Plz keep in mind that this is untested code. I’m not responsible for the consequence.)

Yes, the answer is that RefCounted can be useful in C#.

I wouldn’t suggest changing Dispose of GodotObject. You can think of the engine and the C# memory as somewhat separate. When the object wrapper is disposed in C#, it might still be referenced/be in use by the engine (e.g. think of nodes in the scene-tree). If you want to remove a GodotObject you should explicitly Free it, so that the engine object is deleted.
If you want to have the object automatically freed when there is no reference, you should use RefCounted. But again, just because there is no ‘C# reference’ does not mean that there is no engine reference (e.g., resources attached to nodes). Also note that there can be some delay since the object is only freed when the garbage collector runs (see RefCounted — Godot Engine (stable) documentation in English).

Of course, if you don’t need any engine interop, there is no need to use either. You can and should just use a normal C# object. In this case, the object just resides in the .net managed memory and it is normally garbage collected.

If you are interested it might help to check out the wrapper implementation.

1 Like

Thanks for linking to the source code, that helped quite a bit. I made another test and it looks like RefCounted does what I expected GodotObject to do:

using Godot;
using System;

public partial class PurityTest : Node
{
    public override void _Ready()
    {
        CheckGC();
    }

    public void CheckGC()
    {
        GC.Collect(); // reset for test

        GD.Print($"Making GodotObjects");
        MakeGarbage();
        GD.Print($"GC heap: {GC.GetTotalMemory(false)}");
        GC.Collect();
        GD.Print($"GC heap: {GC.GetTotalMemory(true)}"); // true makes it wait for collection to finish
        GD.Print($"Constructors activated: {FinalSnitch.Created}");
        GD.Print($"Finalizers activated: {FinalSnitch.Finalized}");
        GD.Print($"Objects disposed: {FinalSnitch.TotalDisposed}");
        GD.Print(string.Empty);

        GD.Print($"Making RefCounted");
        MakeGarbageRefCounted();
        GD.Print($"GC heap: {GC.GetTotalMemory(false)}");
        GC.Collect();
        GD.Print($"GC heap: {GC.GetTotalMemory(true)}"); // true makes it wait for collection to finish
        GD.Print($"Constructors activated: {RefSnitch.Created}");
        GD.Print($"Finalizers activated: {RefSnitch.Finalized}");
        GD.Print($"Objects disposed: {RefSnitch.TotalDisposed}");
    }

    private static void MakeGarbage()
    {
        FinalSnitch f;
        for (int i = 0; i < 10000; i++)
        {
            f = new FinalSnitch();
        }
    }

    private static void MakeGarbageRefCounted()
    {
        RefSnitch r;
        for (int i = 0; i < 10000; i++)
        {
            r = new RefSnitch();
        }
    }
}

public partial class FinalSnitch : GodotObject
{
    public static int Created { get; set; }
    public static int Finalized { get; set; }
    public static int TotalDisposed { get; set; }

    public FinalSnitch()
    {
        Created++;
    }

    protected override void Dispose(bool disposing)
    {
        TotalDisposed++;
        base.Dispose(disposing);
    }

    ~FinalSnitch()
    {
        Finalized++;
    }
}

public partial class RefSnitch : RefCounted
{
    public static int Created { get; set; }
    public static int Finalized { get; set; }
    public static int TotalDisposed { get; set; }

    public RefSnitch()
    {
        Created++;
    }

    protected override void Dispose(bool disposing)
    {
        TotalDisposed++;
        base.Dispose(disposing);
    }

    ~RefSnitch()
    {
        Finalized++;
    }
}

Result:

Making GodotObjects
GC heap: 2486496
GC heap: 1588952
Constructors activated: 10000
Finalizers activated: 0
Objects disposed: 0

Making RefCounted
GC heap: 4015192
GC heap: 1902560
Constructors activated: 10000
Finalizers activated: 10000
Objects disposed: 10000

WARNING: ObjectDB instances leaked at exit (run with --verbose for details).
   at: cleanup (core/object/object.cpp:2378)

If I only run the RefCounted part, the memory leak error goes away. :white_check_mark:

Upon further inspection, it looks like the constructor links the object’s GC handle to an internal table the engine keeps. I think this has something to do with why the finalizers don’t get called.

I was unable to find the implementation of Free() or QueueFree() to see what it does differently. The only one I found during a file search was in CustomGCHandle.

Calling Dispose() doesn’t stop the memory leak, but using RefCounted does. I find this very strange, but I’m glad I know now.

private static void MakeGarbage()
    {
        FinalSnitch f;
        for (int i = 0; i < 10000; i++)
        {
            f = new FinalSnitch();
            f.Dispose();
        }
    }

@Armynator I get what you’re saying now. It’s exactly backwards if you expect Dispose() to safely dispose of an unmanaged resource as intended. :rofl:

On most APIs, Dispose() is expected to get called before the GC picks up the object. It’s not some hidden guru feature. It’s a big, standard thing used all the time. It’s supposed to be safe.

The problem here is that it doesn’t actually free the resource in Godot’s case. In fact, garbage collection gets suppressed by GodotObject in my testing. It’s counterintuitive from a dotnet POV.

I opened an issue on GitHub in case you want to track it.