www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - GC: finalization order?!

reply Martin Kinkelin <noone spam.com> writes:
Hi,

I'm very surprised by the GC finalization order (D 2.051, Windows).

Minimalistic test:
----------
import std.stdio;

class Child
{
    this()  { writeln("Child.__ctor()"); }
    ~this() { writeln("Child.__dtor()"); }
}

class Parent
{
    private Child _child;
    this()  { writeln("Parent.__ctor()"); _child = new Child(); }
    ~this() { writeln("Parent.__dtor()"); }
}

int main(string[] args)
{
    auto parent = new Parent();
    return 0;
}
----------

Output:
----------
Parent.__ctor()
Child.__ctor()
Child.__dtor()
Parent.__dtor()
----------

So parent._child gets destructed before parent, although parent
obviously holds a reference to the Child instance. My problem is that
I need to access _child in Parent.__dtor(), which therefore doesn't
work as I expected.
Is this a bug or really intended behaviour?!

Thanks in advance,

Martin
Feb 19 2011
next sibling parent reply bearophile <bearophileHUGS lycos.com> writes:
Martin Kinkelin:

 So parent._child gets destructed before parent, although parent
 obviously holds a reference to the Child instance. My problem is that
 I need to access _child in Parent.__dtor(), which therefore doesn't
 work as I expected.
 Is this a bug or really intended behaviour?!
It's intended, despite being not nice for the programmer. Generally in D finalization order done by the GC is not deterministic (it's not even sure you will have finalizations, I think), so you must design your program in a different way (like using RAII and structs, etc). Python GC is based on enhanced reference counting, so it's deterministic. But D uses a less deterministic GC, like a mark & sweep. Bye, bearophile
Feb 19 2011
parent Martin Kinkelin <noone spam.com> writes:
Thanks guys, I'm honestly impressed by the responsiveness of this
newsgroup!

So I guess I'll try to mess around with GC.add/removeRoot().

For the curious:
Child is a memory buffer using reference counting, and serves multiple
Parent instances (multi-dimensional arrays). The goal is to free the
buffer as soon as all related Parents have been destructed (either
explicitly by clear() or by the GC).
Feb 19 2011
prev sibling parent reply Jonathan M Davis <jmdavisProg gmx.com> writes:
On Saturday 19 February 2011 05:54:43 Martin Kinkelin wrote:
 Hi,
 
 I'm very surprised by the GC finalization order (D 2.051, Windows).
 
 Minimalistic test:
 ----------
 import std.stdio;
 
 class Child
 {
     this()  { writeln("Child.__ctor()"); }
     ~this() { writeln("Child.__dtor()"); }
 }
 
 class Parent
 {
     private Child _child;
     this()  { writeln("Parent.__ctor()"); _child = new Child(); }
     ~this() { writeln("Parent.__dtor()"); }
 }
 
 int main(string[] args)
 {
     auto parent = new Parent();
     return 0;
 }
 ----------
 
 Output:
 ----------
 Parent.__ctor()
 Child.__ctor()
 Child.__dtor()
 Parent.__dtor()
 ----------
 
 So parent._child gets destructed before parent, although parent
 obviously holds a reference to the Child instance. My problem is that
 I need to access _child in Parent.__dtor(), which therefore doesn't
 work as I expected.
 Is this a bug or really intended behaviour?!
 
 Thanks in advance,
IIRC, class destructors aren't supposed to reference any references or pointers to the heap. They're intended for cleaning up other resources. I don't think that there are any guarantees with regards to the order of the destruction of objects which are being garbage collected. But I don't mess with destructors much, so I'm not all that well versed in the details. - Jonathan M Davis
Feb 19 2011
parent reply Johannes Pfau <spam example.com> writes:
Jonathan M Davis wrote:
IIRC, class destructors aren't supposed to reference any references or
pointers to the heap. They're intended for cleaning up other
resources. I don't think that there are any guarantees with regards to
the order of the destruction of objects which are being garbage
collected. But I don't mess with destructors much, so I'm not all that
well versed in the details.

- Jonathan M Davis
I think destructors shouldn't use references to the _garbage collected_ heap. Freeing resources which were allocated with malloc should work. In fact freeing C memory is the only usecase for destructors I can think of. If you rely on destructors to release file handles / gpu textures / other limited resources you risk to run out of those. --=20 Johannes Pfau
Feb 19 2011
parent reply Jonathan M Davis <jmdavisProg gmx.com> writes:
On Saturday 19 February 2011 12:34:39 Johannes Pfau wrote:
 Jonathan M Davis wrote:
IIRC, class destructors aren't supposed to reference any references or
pointers to the heap. They're intended for cleaning up other
resources. I don't think that there are any guarantees with regards to
the order of the destruction of objects which are being garbage
collected. But I don't mess with destructors much, so I'm not all that
well versed in the details.

- Jonathan M Davis
I think destructors shouldn't use references to the _garbage collected_ heap. Freeing resources which were allocated with malloc should work. In fact freeing C memory is the only usecase for destructors I can think of. If you rely on destructors to release file handles / gpu textures / other limited resources you risk to run out of those.
Yes. You're right. They can reference the non-GC heap just fine. It's just that they can't reference the GC heap - probably because the destructor order is indeterminate and so that the GC doesn't have to worry about dealing with circular references between garbage collected objects. - Jonathan M Davis
Feb 19 2011
next sibling parent reply "Simen kjaeraas" <simen.kjaras gmail.com> writes:
Jonathan M Davis <jmdavisProg gmx.com> wrote:

 Yes. You're right. They can reference the non-GC heap just fine. It's  
 just that
 they can't reference the GC heap - probably because the destructor order  
 is
 indeterminate and so that the GC doesn't have to worry about dealing with
 circular references between garbage collected objects.
D could support finalizers in lieu of (or in addition to) destructors. In such a case, they would be called before the object graph were garbage- collected, and one could hence reference other objects on the GC heap. Is there any reason why this approach was not chosen? -- Simen
Feb 20 2011
parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday 20 February 2011 04:10:18 Simen kjaeraas wrote:
 Jonathan M Davis <jmdavisProg gmx.com> wrote:
 Yes. You're right. They can reference the non-GC heap just fine. It's
 just that
 they can't reference the GC heap - probably because the destructor order
 is
 indeterminate and so that the GC doesn't have to worry about dealing with
 circular references between garbage collected objects.
D could support finalizers in lieu of (or in addition to) destructors. In such a case, they would be called before the object graph were garbage- collected, and one could hence reference other objects on the GC heap. Is there any reason why this approach was not chosen?
I'm really not very well versed in the details of the GC, and I don't know all that much about why it works the way that it works. For the most part, though, I don't see much need for either destructors or finalizers in classes. The main reason for them in C++ is for managing memory, which the GC takes care of for you. And since RAII is taken care of by structs, there really isn't much reason for class destructors that I can see - at least not normally. I can see why you might need it occasionally, but for the most part, you don't. Java has finalizers (but no constructors), but I've never needed them, and I've never seen them used. So, while the situation with class destructors and finalizers could be handled better in D, I don't think that it's normally an issue at all. Regardless, I'm not particularly well versed in the details of the GC, so I'm really not the best person to answer questions about it. - Jonathan M Davis
Feb 20 2011
prev sibling parent reply Martin Kinkelin <noone spam.com> writes:
I came to the same conclusion. Even if Parent is a struct, it may get
destructed after its Child (e.g., in case the Parent struct is a field
of another class).
What I did is to use a custom allocator for Child, so that Child
instances are not managed by the GC:

----------
import std.stdio;
import std.c.stdlib;
import core.exception;

private class Child
{
    new(size_t size)
    {
        writeln("Child.new()");
        void* p = malloc(size);
        if (!p)
            onOutOfMemoryError();
        return p;
    }
    delete(void* p)
    {
        writeln("Child.delete()");
        if (p)
            free(p);
    }


    private size_t _refs;

    this()
    {
        writeln("Child.__ctor()");
        _refs = 1;
    }
    ~this()
    {
        writeln("Child.__dtor()");
        if (_refs != 0)
            throw new Exception("still referenced");
    }

    void addRef() { _refs++; }
    void release()
    {
        if (--_refs == 0)
            delete this;
    }
}

class Parent
{
    private Child _child;

    this()
    {
        writeln("Parent.__ctor()");
        _child = new Child();
    }
    ~this()
    {
        writeln("Parent.__dtor()");
        if (_child)
            _child.release();
    }
}

unittest
{
    auto p = new Parent();
}
----------

Output:
----------
Parent.__ctor()
Child.new()
Child.__ctor()
Parent.__dtor()
Child.__dtor()
Child.delete()
----------

This way, Child is destructed as soon as the last related Parent is destructed.
Thanks for clarifying!
Feb 20 2011
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sun, 20 Feb 2011 07:15:06 -0500, Martin Kinkelin <noone spam.com> wrote:

 I came to the same conclusion. Even if Parent is a struct, it may get
 destructed after its Child (e.g., in case the Parent struct is a field
 of another class).
 What I did is to use a custom allocator for Child, so that Child
 instances are not managed by the GC:

 ----------
 import std.stdio;
 import std.c.stdlib;
 import core.exception;

 private class Child
 {
     new(size_t size)
     {
         writeln("Child.new()");
         void* p = malloc(size);
         if (!p)
             onOutOfMemoryError();
         return p;
     }
     delete(void* p)
     {
         writeln("Child.delete()");
         if (p)
             free(p);
     }
I hate to deflate your bubble, but this feature (custom handlers for new and delete) is going to be deprecated. However, you can reinflate with the intended replacement. Essentially, you will move allocation handling outside of child and move it to Parent. Check out emplace (not sure what module it is). -Steve
Feb 20 2011
parent Martin Kinkelin <noone spam.com> writes:
Hehe, thx for deflating and pointing in the right direction.
Feb 20 2011