On Thursday, January 29, 2004, at 08:41 , Dan Sugalski wrote:

So, while I'm heaping much grumpiness on threads (though I suppose, as I've been out of touch for a bit maybe you've all solved the problem. That'd be nice) I've also been thinking about DOD, as there's a fair amount of overlap in the things that cause problems for threads and the ones that cause problems for generational garbage collectors.

Can you elaborate a bit on your concept of generational collection as they apply to parrot? To my ear, generational collection implies all of this (which is a lot, and a lot of it grossly inapplicable to parrot as the runtime now stands):



GENERATIONAL GARBAGE COLLECTORS


Rationale: Most objects are short-lived, and those which are not are less likely to become garbage. Marking or copying the older is statistically wasted effort. A garbage collector which usually traces only new objects will thus tend to perform better than one which traces all of them.

Implementation: Most generational collectors are copying collectors. They are generally mutations upon a hemispace collector. The variation is that collection always proceeds from a small, fixed-sized space to a larger, growable space, rather than swapping back and forth between two symmetric spaces. These asymmetric spaces are the so-called generations, and the small one containing the younger objects is dubbed the nursery. Moving an object from the nursery is called tenuring. Thus, the "generation" of an object is not so much a property of an object as it is a property of the pool from which the object is presently allocated.

Some generational collectors have intermediate generations, but most do not. (Parrot might benefit from a "younger-than-the-nursery" generation for impatient destructors, but then that property would need to be ascertainable at construction time.)

The nursery is typically explicitly un-fragmented. All live objects in it are tenured from it when it fills. Thus, allocation can be as simple as an atomic pointer increment and filling in an object header. The tenured generation might be a heap.

To avoid tracing from the roots, the generational collector requires a cooperative mutator to maintain a table of references from tenured objects to objects within the nursery. This is often forced upon the mutator through the use of VM write barriers. The write barrier function marks the VM page as dirty and then removes write protection for that page. When invoked, the garbage collector will examine all dirtied pages, updating the table of references.

A truly cooperative mutator will update a table of dirty pages every time it changes a pointer variable. Usually, the pages are smaller than VM pages, and thus called "cards" instead. This operation can be as simple as card_table[((int) &ptr) >> n] = 1; ptr = obj;.[1] An expensive proposition, at least doubling the memory bandwidth overhead of pointer manipulation. But, since cards are smaller than pages, it can reduce the amount of memory which has to be scanned at collection time.

Despite being incremental by nature, a generational collector is not a concurrent collector. That is, it still "stops the world," it just doesn't do it for quite a long as a traditional hemispace or mark-sweep collector might.

Being a copying collector in the traditional mold has its drawbacks. It requires accurate reference tracing, in particular. A traditional copying collector cannot work with a conservative collector. For instance, it cannot operate on the OS stack (unless the program is written very cooperatively), since the collector cannot ascertain for certain whether a value is a pointer (which should be traced and possibly updated) or data (which should be left alone).


But parrot has anchored objects and other sorts of things which are incompatible with this. In particular, the fast-path nursery allocator (the very fast allocation being a major benefit of generational collectors) can't work: If an object were anchored in the nursery, it must stay there after GC rather than being tenured; this leaves the nursery fragmented and complicates the very hot allocation code path. Then there's the whole "PMCs don't move" guarantee. And there's also the conservative tracing of system areas.




Gordon Henriksen
[EMAIL PROTECTED]

[1] To be that simple, all allocated memory must be contiguous. So it's not usually that simple. More likely, this:

        (((alloc_header*) obj) - 1)->pool.card_table[((int) obj) >> n];
        obj->ptr = newvalue;

Reply via email to