parrotcode: Incremental mark and sweep garbage collection | |
Contents | C |
src/gc_ims.c - Incremental mark and sweep garbage collection
The following comments describe a new garbage collection scheme for Parrot.
The scheme of this algorithm is described in the literature with these keywords:
- non-copying, mark & sweep
- incremental
- realtime
- incremental update with write barrier
Further we might try this optimization
- treadmill optimization or
- implict reclamation
* can take arbitrary time to complete (1s for 1 Meg objects)
* can't be used in multi-threaded Parrot
* works fast for plain (non-aggregate) objects but suffers badly
for nested aggregates or HLL objects
* the sweep phase takes time proportional to the allocated storage
The incremental mark and sweep collector has an additional structure in the arena_base that keeps track of the collector's state. Pool and arena structures are unchanged. Only the allocation of new arena blocks is done much more fine grained in e.g. 8K blocks.
All objects get two additional pointers (forward, backward) and are arranged like in this scheme:
<-- allocation direction marking -->
| |
[w] <--> [w] <--> [b] <--> [b] <--> [g] <--> [g] <--> [w] <-> [w]
^ ^ ^ ^
| | | |
free-list-ptr to-space scan-pointer from-space
Objects get "moved" during collection by rearranging the doubly-linked object pointers. At the end of a DOD run (when the last grey object is blackened), the from-space and the free-list are merged serving as the new free-list of the next DOD cycle. This operation is just a few pointer manipulations that replaces the sweep phase of a mark and sweep collector.
MS ... mark and sweep (stop-the-world)
IMS ... incremental mark and sweep
IMIR .. incremental mark implicit reclamation
MS IMS IMIR
------------------------------------------------------------------------
operation stop-the-world incremental incremental
time per DOD cycle unbounded bounded bounded
size overhead 1 word 1 word 2 words
time overhead O(2*live + dead) O(2*live + dead) O(live) 2)
Notes:
2) it should be possible to mark containers at once by using the
information of the from-space pointers and tracking changes
to the aggregate.
*/
#include "parrot/parrot.h" #include "parrot/dod.h"
/* HEADERIZER HFILE: include/parrot/dod.h */
/* HEADERIZER BEGIN: static */
static int collect_cb( PARROT_INTERP, NOTNULL(Small_Object_Pool *pool), int flag, NOTNULL(void *arg) ) __attribute__nonnull__(1) __attribute__nonnull__(2) __attribute__nonnull__(4);
static void gc_ims_add_free_object( PARROT_INTERP, NOTNULL(Small_Object_Pool *pool), NOTNULL(PObj *to_add) ) __attribute__nonnull__(1) __attribute__nonnull__(2) __attribute__nonnull__(3);
static void gc_ims_alloc_objects( PARROT_INTERP, NOTNULL(Small_Object_Pool *pool) ) __attribute__nonnull__(1) __attribute__nonnull__(2);
PARROT_CANNOT_RETURN_NULL PARROT_WARN_UNUSED_RESULT static PObj * gc_ims_get_free_object( PARROT_INTERP, NOTNULL(Small_Object_Pool *pool) ) __attribute__nonnull__(1) __attribute__nonnull__(2);
static void gc_ims_pool_init( SHIM_INTERP, NOTNULL(Small_Object_Pool *pool) ) __attribute__nonnull__(2);
static int parrot_gc_ims_collect( PARROT_INTERP, int check_only ) __attribute__nonnull__(1);
static void parrot_gc_ims_deinit( PARROT_INTERP ) __attribute__nonnull__(1);
static void parrot_gc_ims_mark( PARROT_INTERP ) __attribute__nonnull__(1);
static void parrot_gc_ims_reinit( PARROT_INTERP ) __attribute__nonnull__(1);
static void parrot_gc_ims_run( PARROT_INTERP, int flags ) __attribute__nonnull__(1);
static void parrot_gc_ims_run_increment( PARROT_INTERP ) __attribute__nonnull__(1);
static void parrot_gc_ims_sweep( PARROT_INTERP ) __attribute__nonnull__(1);
static int sweep_cb( PARROT_INTERP, NOTNULL(Small_Object_Pool *pool), int flag, NOTNULL(void *arg) ) __attribute__nonnull__(1) __attribute__nonnull__(2) __attribute__nonnull__(4);
/* HEADERIZER END: static */
/* * size of one arena */ #define ALLOCATION_BLOCK_SIZE 8192
/* * each ALLOCATIONS_INIT allocations of any object an incremental * step is triggered */ #define ALLOCATIONS_INIT 1024*4
/* * a mark step does allocations * throttle work */ #define THROTTLE 1.3
/* * if we have at the end total * refill free objects * we just do nothing */ #define REFILL_FACTOR 0.5
/* * we run the copying collector, if memory pool statistics indicate * that this amount of the total size could be freed * * This factor also depends on the allocation color of buffer headers, * which is set to black now. So we are always one DOD cycle behind * and the statistics are rather wrong. */ #define MEM_POOL_RECLAIM 0.2
#if 0 # define IMS_DEBUG(x) fprintf x #else # define IMS_DEBUG(x) #endif
typedef enum { /* these states have to be in execution order */ GC_IMS_INITIAL, /* memory subsystem setup */ GC_IMS_STARTING, /* wait for DOD_block to clear */ GC_IMS_RE_INIT, /* start of normal operation - mark root */ GC_IMS_MARKING, /* mark children */ GC_IMS_START_SWEEP, /* mark finished, start sweep buffers */ GC_IMS_SWEEP, /* sweep buffers */ GC_IMS_COLLECT, /* collect buffer memory */ GC_IMS_FINISHED, /* update statistics */ GC_IMS_CONSUMING, /* when we have plenty of free objects */ GC_IMS_DEAD /* gc is already shutdown */
} gc_ims_state_enum;
typedef struct Gc_ims_private { gc_ims_state_enum state; size_t allocations; /* get_free_object count */ size_t alloc_trigger; /* after this number of allocations a gc increment is triggered */ double throttle; /* throttle * allocations per increment work */ size_t increments; /* increment count */ int lazy; /* timely destruction run */ size_t n_objects; /* live count of prev run */ size_t n_extended_PMCs;/* PMCs found during mark_special */ } Gc_ims_private;
/*
FUNCDOC: gc_ims_add_free_object
Add object to_add
to the free_list in the given pool. pool-
num_free_objects> has to be updated by the caller.
FUNCDOC: gc_ims_get_free_object
Get a new object off the free_list in the given pool.
FUNCDOC: gc_ims_alloc_objects
Allocate new objects for the given pool.
*/
static void gc_ims_add_free_object(PARROT_INTERP, NOTNULL(Small_Object_Pool *pool), NOTNULL(PObj *to_add)) { *(void **)to_add = pool->free_list; pool->free_list = to_add; #if DISABLE_GC_DEBUG UNUSED(interp); #else if (GC_DEBUG(interp)) { if (pool == interp->arena_base->pmc_pool) { PMC * const p = (PMC *)to_add; p->vtable = interp->vtables[enum_class_Null]; } } #endif }
PARROT_CANNOT_RETURN_NULL PARROT_WARN_UNUSED_RESULT static PObj * gc_ims_get_free_object(PARROT_INTERP, NOTNULL(Small_Object_Pool *pool)) { PObj *ptr; Arenas * const arena_base = interp->arena_base; Gc_ims_private * const g_ims = (Gc_ims_private *)arena_base->gc_private;
if (++g_ims->allocations >= g_ims->alloc_trigger) {
g_ims->allocations = 0;
parrot_gc_ims_run_increment(interp);
}
/* if we don't have any objects */
if (!pool->free_list)
(*pool->alloc_objects) (interp, pool);
ptr = (PObj *)pool->free_list;
pool->free_list = *(void **)ptr;
/*
* buffers are born black, PMCs not yet?
* XXX this does not solve the problem of storing keys in hashes
* in the next DOD cycle (if the key isn't marked elsewhere ?)
*/
PObj_flags_SETTO(ptr, pool == arena_base->pmc_pool ? 0 : PObj_live_FLAG);
--pool->num_free_objects;
return ptr;
}
static void gc_ims_alloc_objects(PARROT_INTERP, NOTNULL(Small_Object_Pool *pool)) { Small_Object_Arena *new_arena; size_t size;
pool->objects_per_alloc = ALLOCATION_BLOCK_SIZE / pool->object_size;
/* Setup memory for the new objects */
new_arena = mem_allocate_typed(Small_Object_Arena);
size = ALLOCATION_BLOCK_SIZE;
new_arena->start_objects = mem_sys_allocate(size);
Parrot_append_arena_in_pool(interp, pool, new_arena, size);
Parrot_add_to_free_list(interp, pool, new_arena);
}
static void gc_ims_pool_init(SHIM_INTERP, NOTNULL(Small_Object_Pool *pool)) { pool->add_free_object = gc_ims_add_free_object; pool->get_free_object = gc_ims_get_free_object; pool->alloc_objects = gc_ims_alloc_objects; pool->more_objects = pool->alloc_objects; }
static void parrot_gc_ims_deinit(PARROT_INTERP) { Arenas * const arena_base = interp->arena_base;
mem_sys_free(arena_base->gc_private);
arena_base->gc_private = NULL;
}
/*
FUNCDOC: Parrot_gc_ims_init
Initialize the state structures of the gc system. Called immediately before creation of memory pools. This function must set the function pointers for add_free_object_fn
, get_free_object_fn
, alloc_objects_fn
, and more_objects_fn
.
*/
PARROT_API void Parrot_gc_ims_init(PARROT_INTERP) { Arenas * const arena_base = interp->arena_base;
arena_base->gc_private = mem_sys_allocate_zeroed(sizeof (Gc_ims_private));
/*
* set function hooks according to pdd09
*/
arena_base->do_dod_run = parrot_gc_ims_run;
arena_base->de_init_gc_system = parrot_gc_ims_deinit;
arena_base->init_pool = gc_ims_pool_init;
/*
* run init state
*/
parrot_gc_ims_run_increment(interp);
}
/*
FUNCDOC: parrot_gc_ims_reinit
Reinitialize the collector for the next collection cycle.
*/
static void parrot_gc_ims_reinit(PARROT_INTERP) { Gc_ims_private *g_ims; Arenas * const arena_base = interp->arena_base;
arena_base->lazy_dod = 0;
Parrot_dod_ms_run_init(interp);
/*
* trace root set w/o system areas
* TODO also skip volatile roots
*/
Parrot_dod_trace_root(interp, 0);
g_ims = (Gc_ims_private *)arena_base->gc_private;
g_ims->state = GC_IMS_MARKING;
}
/*
FUNCDOC: parrot_gc_ims_mark
Mark a bunch of children.
The work depends on item counts with and without a next_for_GC field. The former are marked immediately, only the latter need real work here.
*/
static void parrot_gc_ims_mark(PARROT_INTERP) { size_t todo; double work_factor; PMC *next;
Arenas * const arena_base = (Arenas *)interp->arena_base;
Gc_ims_private * const g_ims = (Gc_ims_private *)arena_base->gc_private;
/*
* use statistics from the previous run
*/
if (g_ims->n_objects)
work_factor = (double)g_ims->n_extended_PMCs / g_ims->n_objects;
else
work_factor = 1.0;
todo = (size_t)(g_ims->alloc_trigger * g_ims->throttle * work_factor);
PARROT_ASSERT(arena_base->lazy_dod == 0);
Parrot_dod_trace_children(interp, todo);
/*
* check if we are finished with marking - the end is
* self-referential
*/
next = arena_base->dod_mark_start;
if (next == PMC_next_for_GC(next)) {
g_ims->state = GC_IMS_START_SWEEP;
}
}
/*
FUNCDOC: parrot_gc_ims_sweep
Free unused objects in all header pools.
TODO split work per pool.
*/
static int sweep_cb(PARROT_INTERP, NOTNULL(Small_Object_Pool *pool), int flag, NOTNULL(void *arg)) { int * const n_obj = (int *) arg;
Parrot_dod_sweep(interp, pool);
if (interp->profile && (flag & POOL_PMC))
Parrot_dod_profile_end(interp, PARROT_PROF_DOD_cp);
*n_obj += pool->total_objects - pool->num_free_objects;
return 0;
}
static void parrot_gc_ims_sweep(PARROT_INTERP) { Arenas * const arena_base = interp->arena_base; Gc_ims_private *g_ims = (Gc_ims_private *)arena_base->gc_private; size_t n_objects; int ignored;
IMS_DEBUG((stderr, "\nSWEEP\n"));
/*
* as we are now gonna kill objects, make sure that we
* have traced the current stack
* except for a lazy run, which is invoked from the run loop
*/
/* TODO mark volatile roots */
Parrot_dod_trace_root(interp, g_ims->lazy ? 0 : DOD_trace_stack_FLAG);
/* mark (again) rest of children */
Parrot_dod_trace_children(interp, (size_t) -1);
/* now sweep all */
n_objects = 0;
ignored = Parrot_forall_header_pools(interp, POOL_BUFFER | POOL_PMC,
(void*)&n_objects, sweep_cb);
if (interp->profile)
Parrot_dod_profile_end(interp, PARROT_PROF_DOD_cb);
g_ims->state = GC_IMS_COLLECT;
g_ims->n_objects = n_objects;
g_ims->n_extended_PMCs = arena_base->num_extended_PMCs;
}
/*
FUNCDOC: parrot_gc_ims_collect
Run the copying collector in memory pools, if it could yield some free memory.
*/
#if !defined(GC_IS_MALLOC) || !GC_IS_MALLOC static int collect_cb(PARROT_INTERP, NOTNULL(Small_Object_Pool *pool), int flag, NOTNULL(void *arg)) { const int check_only = (int)(INTVAL)arg; Memory_Pool *mem_pool; /* * check if there is an associated memory pool */ mem_pool = pool->mem_pool; if (!mem_pool) return 0; /* * and if the memory pool supports compaction */ if (!mem_pool->compact) return 0; /* * several header pools can share one memory pool * if that pool is already compacted, the following is zero */ if (!mem_pool->guaranteed_reclaimable) return 0; /* * check used size */ if ((mem_pool->possibly_reclaimable * mem_pool->reclaim_factor + mem_pool->guaranteed_reclaimable) >= mem_pool->total_allocated * MEM_POOL_RECLAIM) { IMS_DEBUG((stderr, "COMPACT\n")); if (check_only) return 1; mem_pool->compact(interp, mem_pool); } return 0; } #endif
static int parrot_gc_ims_collect(PARROT_INTERP, int check_only) { #if defined(GC_IS_MALLOC) && GC_IS_MALLOC UNUSED(interp); UNUSED(check_only); #else Arenas * const arena_base = interp->arena_base; Gc_ims_private *g_ims; int ret;
if (!check_only && interp->profile)
Parrot_dod_profile_start(interp);
g_ims = (Gc_ims_private *)arena_base->gc_private;
ret = Parrot_forall_header_pools(interp, POOL_BUFFER,
(void*)check_only, collect_cb);
if (ret)
return ret;
if (check_only)
return 0;
if (interp->profile)
Parrot_dod_profile_end(interp, PARROT_PROF_GC);
g_ims->state = GC_IMS_FINISHED;
#endif
return 0;
}
/*
FUNCDOC: parrot_gc_ims_run_increment
Run one increment of collection. This function is triggered by object allocation.
*/
static void parrot_gc_ims_run_increment(PARROT_INTERP) { Arenas * const arena_base = interp->arena_base; Gc_ims_private * const g_ims = (Gc_ims_private *)arena_base->gc_private;
if (arena_base->DOD_block_level || g_ims->state == GC_IMS_DEAD)
return;
++g_ims->increments;
IMS_DEBUG((stderr, "state = %d => ", g_ims->state));
switch (g_ims->state) {
case GC_IMS_INITIAL:
g_ims->state = GC_IMS_STARTING;
g_ims->alloc_trigger = ALLOCATIONS_INIT;
g_ims->throttle = THROTTLE;
break;
case GC_IMS_STARTING:
/* fall through and start */
/* FALLTHRU */
case GC_IMS_RE_INIT:
parrot_gc_ims_reinit(interp);
break;
case GC_IMS_MARKING:
parrot_gc_ims_mark(interp);
break;
case GC_IMS_START_SWEEP:
g_ims->state = GC_IMS_SWEEP;
/* fall through */
case GC_IMS_SWEEP:
parrot_gc_ims_sweep(interp);
/* fall through */
case GC_IMS_COLLECT:
(void)parrot_gc_ims_collect(interp, 0);
break;
case GC_IMS_FINISHED:
++arena_base->dod_runs;
g_ims->state = GC_IMS_CONSUMING;
/* fall through */
case GC_IMS_CONSUMING:
/*
* This currently looks only at PMCs and string_headers.
* There shouldn't be other pools that could run out of
* headers independent of PMCs
*/
if (arena_base->pmc_pool->num_free_objects <
arena_base->pmc_pool->total_objects * REFILL_FACTOR) {
g_ims->state = GC_IMS_STARTING;
}
else if (arena_base->string_header_pool->num_free_objects <
arena_base->string_header_pool->total_objects *
REFILL_FACTOR) {
g_ims->state = GC_IMS_STARTING;
}
break;
default:
PANIC(interp, "Unknown state in gc_ims");
}
IMS_DEBUG((stderr, "%d\n", g_ims->state));
}
/*
FUNCDOC: parrot_gc_ims_run
Interface to Parrot_do_dod_run
. flags
is one of:
DOD_lazy_FLAG ... timely destruction
DOD_finish_FLAG ... run until live bits are clear
*/
static void parrot_gc_ims_run(PARROT_INTERP, int flags) { int lazy; Arenas * const arena_base = interp->arena_base; Gc_ims_private * const g_ims = (Gc_ims_private *)arena_base->gc_private;
if (arena_base->DOD_block_level || g_ims->state == GC_IMS_DEAD) {
return;
}
if (flags & DOD_finish_FLAG) {
/*
* called from really_destroy. This interpreter is gonna die.
* The destruction includes a sweep over PMCs, so that
* destructors/finalizers are called.
*
* Be sure live bits are clear.
*/
if (g_ims->state >= GC_IMS_RE_INIT || g_ims->state < GC_IMS_FINISHED)
Parrot_dod_clear_live_bits(interp);
Parrot_dod_sweep(interp, interp->arena_base->pmc_pool);
g_ims->state = GC_IMS_DEAD;
return;
}
/* make the test happy that checks the count ;) */
arena_base->dod_runs++;
lazy = flags & DOD_lazy_FLAG;
if (!lazy) {
/* run a full cycle
* TODO if we are called from mem_allocate() in src/resources.c:
* * pass needed size
* * test examples/benchmarks/gc_header_new.pasm
*/
if (!parrot_gc_ims_collect(interp, 1)) {
parrot_gc_ims_run_increment(interp);
return;
}
if (g_ims->state >= GC_IMS_FINISHED)
g_ims->state = GC_IMS_STARTING;
while (1) {
parrot_gc_ims_run_increment(interp);
if (g_ims->state > GC_IMS_COLLECT)
break;
}
return;
}
/*
* lazy DOD handling
*/
IMS_DEBUG((stderr, "\nLAZY state = %d\n", g_ims->state));
g_ims->lazy = lazy;
if (g_ims->state >= GC_IMS_COLLECT) {
/* we are beyond sweep, timely destruction is done */
if (arena_base->num_early_PMCs_seen >= arena_base->num_early_DOD_PMCs)
return;
/* when not all seen, start a fresh cycle */
g_ims->state = GC_IMS_RE_INIT;
/* run init, which clears lazy seen counter */
parrot_gc_ims_run_increment(interp);
}
/*
* run through all steps until we see enough PMCs that need timely
* destruction or we finished sweeping
*/
while (arena_base->num_early_PMCs_seen < arena_base->num_early_DOD_PMCs) {
parrot_gc_ims_run_increment(interp);
if (g_ims->state >= GC_IMS_COLLECT)
break;
}
/*
* if we stopped early, the lazy run was successful
*/
if (g_ims->state < GC_IMS_COLLECT)
++arena_base->lazy_dod_runs;
g_ims->lazy = 0;
}
/*
FUNCDOC: Parrot_dod_ims_wb
Write barrier called by the DOD_WRITE_BARRIER macro. Always when storing a white object into a black aggregate, either the object must be greyed or the aggregate must be rescanned - by greying it.
*/
#define DOD_IMS_GREY_NEW 1
PARROT_API void Parrot_dod_ims_wb(PARROT_INTERP, NOTNULL(PMC *agg), NOTNULL(PMC *_new)) { #if DOD_IMS_GREY_NEW IMS_DEBUG((stderr, "%d agg %p mark %p\n", ((Gc_ims_private *)interp->arena_base-> gc_private)->state, agg, _new)); pobject_lives(interp, (PObj*)_new); #else PObj_get_FLAGS(agg) &= ~ (PObj_live_FLAG|PObj_custom_GC_FLAG); pobject_lives(interp, (PObj*)agg); #endif }
/*
src/dod.c, include/parrot/dod.h, include/parrot/pobj.h,
Initial version by leo (2004.08.12 - 2004.08.15)
*/
/* * Local variables: * c-file-style: "parrot" * End: * vim: expandtab shiftwidth=4: */
|