1288 lines
30 KiB
C
1288 lines
30 KiB
C
#define _POSIX_C_SOURCE 199309L
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <inttypes.h>
|
|
#include <setjmp.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "gc.h"
|
|
#include "builtin.h"
|
|
|
|
#if 1
|
|
#define ENABLE_BACKTRACE
|
|
#include <execinfo.h>
|
|
#endif
|
|
|
|
/****************************************************************************/
|
|
|
|
#if _CLOCK_MONOTONIC
|
|
# define TIMING_CLOCK CLOCK_MONOTONIC
|
|
#else
|
|
# define TIMING_CLOCK CLOCK_REALTIME
|
|
#endif
|
|
|
|
#define FLAG_PROC_BIT ((uint8_t)0x20)
|
|
#define FLAG_LIVE_BIT ((uint8_t)0x10)
|
|
|
|
#define FLAG_GC_BITS ((uint8_t)0x30)
|
|
#define FLAG_TAG_BITS ((uint8_t)0x0f)
|
|
|
|
/* Helper macros to reduce duplication */
|
|
#define VECTOR_BYTES(nelem) (sizeof(vector_t) + (sizeof(value_t) * (nelem)))
|
|
#define BYTESTR_BYTES(nbytes) (sizeof(byte_string_t) + (nbytes))
|
|
#define STRUCT_BYTES(nslots) (sizeof(struct_t) + (sizeof(value_t) * (nslots)))
|
|
|
|
#define GC_MIN(x, y) (((y)<(x))?(y):(x))
|
|
#define GC_MAX(x, y) (((y)>(x))?(y):(x))
|
|
|
|
#define GC_DEBUG_LEVEL_TRACE 3
|
|
#define GC_DEBUG_LEVEL_INFO 2
|
|
#define GC_DEBUG_LEVEL_WARN 1
|
|
#define GC_DEBUG_LEVEL_QUIET 0
|
|
|
|
#define debug_warn(fmt, args...) \
|
|
((gc_debug_level >= GC_DEBUG_LEVEL_WARN) ? (void)(fprintf(stderr,fmt,##args)) : (void)0)
|
|
|
|
#define debug_info(fmt, args...) \
|
|
((gc_debug_level >= GC_DEBUG_LEVEL_INFO) ? (void)(fprintf(stderr,fmt,##args)) : (void)0)
|
|
|
|
#define debug_trace(fmt, args...) \
|
|
((gc_debug_level >= GC_DEBUG_LEVEL_TRACE) ? (void)(fprintf(stderr,fmt,##args)) : (void)0)
|
|
|
|
/****************************************************************************/
|
|
|
|
typedef struct seen_value
|
|
{
|
|
value_t value;
|
|
struct seen_value *prev;
|
|
} seen_value_t;
|
|
|
|
/****************************************************************************/
|
|
|
|
const char *const object_tag_names[16] = {
|
|
"special", "box", "weak box", "pair", "fpnum", "builtin", "(6)", "(7)",
|
|
"vector", "byte string", "struct", "will", "(12)", "(13)", "(14)", "(15)"
|
|
};
|
|
|
|
object_block_t object_blocks[OBJECT_BLOCK_MAX + 1];
|
|
gc_stats_t gc_stats;
|
|
|
|
static value_t gc_free_list;
|
|
static value_t gc_weak_box_list;
|
|
static value_t gc_will_list;
|
|
static value_t gc_will_active_list;
|
|
static value_t gc_next_live_value;
|
|
|
|
static size_t gc_live_bytes; /* as of last GC pass */
|
|
static size_t gc_total_bytes; /* as of last allocate_object()/free_object() or GC pass */
|
|
|
|
static bool gc_enabled;
|
|
|
|
static gc_root_t gc_root_list = {
|
|
.value = UNDEFINED,
|
|
.prev = &gc_root_list,
|
|
.next = &gc_root_list
|
|
};
|
|
|
|
static gc_root_t structure_type_root;
|
|
|
|
static int gc_debug_level = GC_DEBUG_LEVEL_QUIET;
|
|
|
|
/****************************************************************************/
|
|
|
|
static inline value_t value_from_index(int blk, int idx);
|
|
|
|
static void allocate_block(void);
|
|
static value_t allocate_object(int tag);
|
|
static void free_object(value_t value);
|
|
static void clear_gc_flag_bits(void);
|
|
|
|
static inline bool is_object_live(value_t value);
|
|
static inline bool is_object_processed(value_t value);
|
|
static inline void set_object_live(value_t value);
|
|
static inline void set_object_processed(value_t value);
|
|
static inline void clear_object_live(value_t value);
|
|
static inline void clear_object_processed(value_t value);
|
|
|
|
/* The 'will' accessors are private to the GC, unlike other similar routines. */
|
|
static inline bool is_will(value_t value);
|
|
static inline will_t *get_will(value_t value);
|
|
static value_t make_will(value_t value, value_t finalizer);
|
|
|
|
/****************************************************************************/
|
|
|
|
static inline uint8_t *_get_flags(value_t value)
|
|
{
|
|
return &object_blocks[OBJECT_BLOCK(value)].flag_bits[OBJECT_INDEX(value)];
|
|
}
|
|
|
|
static inline value_t value_from_index(int blk, int idx)
|
|
{
|
|
int tag = (int)object_blocks[blk].flag_bits[idx] & FLAG_TAG_BITS;
|
|
|
|
/* Unallocated objects have zeroed flag bitfields */
|
|
if (tag == 0) return UNDEFINED;
|
|
|
|
return OBJECT(blk, idx, tag);
|
|
}
|
|
|
|
static void allocate_block(void)
|
|
{
|
|
int blk;
|
|
|
|
for (blk = 0; blk <= OBJECT_BLOCK_MAX; ++blk)
|
|
{
|
|
object_block_t *block = &object_blocks[blk];
|
|
|
|
/* stop at first unallocated block */
|
|
if (!block->objects)
|
|
{
|
|
int idx;
|
|
|
|
block->objects = (object_t*)calloc(OBJECT_INDEX_MAX+1, sizeof(object_t));
|
|
|
|
if (!block->objects)
|
|
{
|
|
release_assert(NOTREACHED("out of memory"));
|
|
return;
|
|
}
|
|
|
|
block->flag_bits = (uint8_t*)calloc(OBJECT_INDEX_MAX+1, sizeof(uint8_t));
|
|
|
|
if (!block->flag_bits)
|
|
{
|
|
free(block->objects);
|
|
block->objects = NULL;
|
|
release_assert(NOTREACHED("out of memory"));
|
|
return;
|
|
}
|
|
|
|
/* Connect the objects inside the block into a linked list */
|
|
for (idx = 0; idx < OBJECT_INDEX_MAX; ++idx)
|
|
{
|
|
/* Any object type would work here; box is simplest */
|
|
block->objects[idx].next = OBJECT(blk, idx+1, OBJECT_TAG_BOX);
|
|
}
|
|
|
|
/* Prepend the list of objects in the block to the free list */
|
|
block->objects[OBJECT_INDEX_MAX].next = gc_free_list;
|
|
gc_free_list = OBJECT(blk, 0, OBJECT_TAG_BOX);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* reached end of block array without finding any unallocated */
|
|
release_assert(NOTREACHED("out of object blocks"));
|
|
}
|
|
|
|
static value_t allocate_object(int tag)
|
|
{
|
|
assert((0 <= tag) && (tag <= 15));
|
|
|
|
/*
|
|
** Run GC when total memory used is either twice the total
|
|
** live after last GC pass, or 16MB, whichever is greater.
|
|
*/
|
|
if (gc_total_bytes >= GC_MAX(2 * gc_live_bytes, 16 * 1024 * 1024))
|
|
{
|
|
if (gc_enabled)
|
|
{
|
|
collect_garbage();
|
|
}
|
|
}
|
|
|
|
if (is_nil(gc_free_list))
|
|
{
|
|
allocate_block();
|
|
assert(!is_nil(gc_free_list));
|
|
}
|
|
|
|
{
|
|
int blk = OBJECT_BLOCK(gc_free_list);
|
|
int idx = OBJECT_INDEX(gc_free_list);
|
|
value_t result = OBJECT(blk, idx, tag);
|
|
|
|
gc_free_list = _get_object(gc_free_list)->next;
|
|
gc_total_bytes += sizeof(object_t);
|
|
|
|
*_get_flags(result) = (uint8_t)tag;
|
|
memset(_get_object(result), 0, sizeof(object_t));
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
static void free_object(value_t value)
|
|
{
|
|
object_t *obj = _get_object(value);
|
|
|
|
if (is_vector(value))
|
|
{
|
|
gc_total_bytes -= VECTOR_BYTES(obj->vector->nelements);
|
|
free(obj->vector);
|
|
}
|
|
else if (is_byte_string(value))
|
|
{
|
|
gc_total_bytes -= BYTESTR_BYTES(obj->byte_string->nbytes);
|
|
free(obj->byte_string);
|
|
}
|
|
else if (is_struct(value))
|
|
{
|
|
gc_total_bytes -= STRUCT_BYTES(obj->structure->nslots);
|
|
free(obj->structure);
|
|
}
|
|
else if (is_will(value))
|
|
{
|
|
gc_total_bytes -= sizeof(will_t);
|
|
free(obj->will);
|
|
}
|
|
|
|
*_get_flags(value) = 0;
|
|
obj->next = gc_free_list;
|
|
|
|
gc_total_bytes -= sizeof(object_t);
|
|
gc_free_list = value;
|
|
}
|
|
|
|
/* Prepare for GC by clearing all "live" and "processed" bits */
|
|
static void clear_gc_flag_bits(void)
|
|
{
|
|
int blk;
|
|
|
|
for (blk = 0; (blk <= OBJECT_BLOCK_MAX) && object_blocks[blk].objects; ++blk)
|
|
{
|
|
object_block_t *block = &object_blocks[blk];
|
|
int idx;
|
|
|
|
for (idx = 0; idx <= OBJECT_INDEX_MAX; ++idx)
|
|
{
|
|
block->flag_bits[idx] &= ~FLAG_GC_BITS;
|
|
}
|
|
}
|
|
}
|
|
|
|
static inline bool is_object_live(value_t v)
|
|
{
|
|
/* non-objects can't be added to the free list, so they're always "live" */
|
|
return !is_object(v) || (*_get_flags(v) & FLAG_LIVE_BIT) != 0;
|
|
}
|
|
|
|
static inline bool is_object_processed(value_t v)
|
|
{
|
|
/* non-objects don't need to be processed */
|
|
return !is_object(v) || (*_get_flags(v) & FLAG_PROC_BIT) != 0;
|
|
}
|
|
|
|
static inline void set_object_live(value_t v)
|
|
{
|
|
assert(is_object(v));
|
|
*_get_flags(v) |= FLAG_LIVE_BIT;
|
|
}
|
|
|
|
static inline void set_object_processed(value_t v)
|
|
{
|
|
assert(is_object(v));
|
|
*_get_flags(v) |= FLAG_PROC_BIT;
|
|
}
|
|
|
|
static inline void clear_object_live(value_t v)
|
|
{
|
|
assert(is_object(v));
|
|
*_get_flags(v) &= ~FLAG_LIVE_BIT;
|
|
}
|
|
|
|
static inline void clear_object_processed(value_t v)
|
|
{
|
|
assert(is_object(v));
|
|
*_get_flags(v) &= ~FLAG_PROC_BIT;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
|
|
static inline bool is_will(value_t value)
|
|
{
|
|
return is_object_type(value, OBJECT_TAG_WILL);
|
|
}
|
|
|
|
static inline will_t *get_will(value_t value)
|
|
{
|
|
return _get_typed_object(value, OBJECT_TAG_WILL)->will;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
|
|
void register_gc_root(gc_root_t *root, value_t v)
|
|
{
|
|
root->value = v;
|
|
root->next = &gc_root_list;
|
|
gc_root_list.prev->next = root;
|
|
root->prev = gc_root_list.prev;
|
|
gc_root_list.prev = root;
|
|
}
|
|
|
|
void unregister_gc_root(gc_root_t *root)
|
|
{
|
|
assert(root && root->prev && root->next); /* Uninitialized */
|
|
assert((root->prev != root) && (root->next != root)); /* Already removed */
|
|
|
|
/* Cut the given root out of the list */
|
|
root->prev->next = root->next;
|
|
root->next->prev = root->prev;
|
|
|
|
/* Remove dead references to root list; protects against double-removal */
|
|
root->prev = root->next = root;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
|
|
value_t make_box(value_t init)
|
|
{
|
|
gc_root_t init_root;
|
|
value_t result;
|
|
|
|
register_gc_root(&init_root, init);
|
|
result = allocate_object(OBJECT_TAG_BOX);
|
|
unregister_gc_root(&init_root);
|
|
|
|
get_box(result)->value = init;
|
|
|
|
return result;
|
|
}
|
|
|
|
value_t make_weak_box(value_t init)
|
|
{
|
|
gc_root_t init_root;
|
|
value_t result;
|
|
|
|
register_gc_root(&init_root, init);
|
|
result = allocate_object(OBJECT_TAG_WEAK_BOX);
|
|
unregister_gc_root(&init_root);
|
|
|
|
get_weak_box(result)->value = init;
|
|
get_weak_box(result)->next = gc_weak_box_list;
|
|
|
|
gc_weak_box_list = result;
|
|
|
|
return result;
|
|
}
|
|
|
|
value_t make_pair(value_t car, value_t cdr)
|
|
{
|
|
gc_root_t car_root;
|
|
gc_root_t cdr_root;
|
|
value_t result;
|
|
|
|
register_gc_root(&car_root, car);
|
|
register_gc_root(&cdr_root, cdr);
|
|
|
|
result = allocate_object(OBJECT_TAG_PAIR);
|
|
|
|
unregister_gc_root(&car_root);
|
|
unregister_gc_root(&cdr_root);
|
|
|
|
get_pair(result)->car = car;
|
|
get_pair(result)->cdr = cdr;
|
|
|
|
return result;
|
|
}
|
|
|
|
value_t make_float(fpnum_t value)
|
|
{
|
|
value_t result = allocate_object(OBJECT_TAG_FPNUM);
|
|
_get_object(result)->fpnum = value;
|
|
return result;
|
|
}
|
|
|
|
value_t make_builtin_fn(builtin_fn_t *fn)
|
|
{
|
|
value_t result = allocate_object(OBJECT_TAG_BUILTIN_FN);
|
|
_get_object(result)->builtin_fn = fn;
|
|
return result;
|
|
}
|
|
|
|
value_t make_vector(size_t nelem, value_t init)
|
|
{
|
|
gc_root_t init_root;
|
|
value_t result;
|
|
vector_t *vec;
|
|
|
|
register_gc_root(&init_root, init);
|
|
result = allocate_object(OBJECT_TAG_VECTOR);
|
|
unregister_gc_root(&init_root);
|
|
|
|
vec = (vector_t*)malloc(VECTOR_BYTES(nelem));
|
|
release_assert(vec != NULL);
|
|
|
|
gc_total_bytes += VECTOR_BYTES(nelem);
|
|
_get_object(result)->vector = vec;
|
|
|
|
vec->immutable = false;
|
|
vec->nelements = nelem;
|
|
|
|
for (size_t i = 0; i < nelem; ++i)
|
|
vec->elements[i] = init;
|
|
|
|
return result;
|
|
}
|
|
|
|
value_t make_byte_string(size_t nbytes, int init)
|
|
{
|
|
value_t result;
|
|
byte_string_t *bstr;
|
|
|
|
result = allocate_object(OBJECT_TAG_BYTE_STRING);
|
|
|
|
bstr = (byte_string_t*)malloc(BYTESTR_BYTES(nbytes));
|
|
release_assert(bstr != NULL);
|
|
|
|
gc_total_bytes += BYTESTR_BYTES(nbytes);
|
|
_get_object(result)->byte_string = bstr;
|
|
|
|
bstr->immutable = false;
|
|
bstr->nbytes = nbytes;
|
|
|
|
memset(bstr->bytes, init, nbytes);
|
|
|
|
return result;
|
|
}
|
|
|
|
value_t make_struct(value_t type)
|
|
{
|
|
gc_root_t type_root;
|
|
fixnum_t nslots;
|
|
value_t result;
|
|
struct_t *str;
|
|
|
|
register_gc_root(&type_root, type);
|
|
|
|
release_assert(struct_is_a(type, get_structure_type()));
|
|
release_assert(get_struct(type)->immutable);
|
|
|
|
nslots = get_fixnum(SLOT_VALUE(STRUCTURE, type, NSLOTS));
|
|
|
|
result = allocate_object(OBJECT_TAG_STRUCT);
|
|
|
|
unregister_gc_root(&type_root);
|
|
|
|
str = (struct_t*)malloc(STRUCT_BYTES(nslots));
|
|
release_assert(str != NULL);
|
|
|
|
gc_total_bytes += STRUCT_BYTES(nslots);
|
|
_get_object(result)->structure = str;
|
|
|
|
str->immutable = false;
|
|
str->nslots = nslots;
|
|
str->type = type;
|
|
|
|
for (int i = 0; i < nslots; ++i)
|
|
str->slots[i] = UNDEFINED;
|
|
|
|
return result;
|
|
}
|
|
|
|
/* wills can only be created in this module, via register_finalizer() */
|
|
static value_t make_will(value_t value, value_t finalizer)
|
|
{
|
|
gc_root_t value_root;
|
|
gc_root_t finalizer_root;
|
|
value_t result;
|
|
will_t *will;
|
|
|
|
register_gc_root(&value_root, value);
|
|
register_gc_root(&finalizer_root, finalizer);
|
|
|
|
result = allocate_object(OBJECT_TAG_WILL);
|
|
|
|
unregister_gc_root(&value_root);
|
|
unregister_gc_root(&finalizer_root);
|
|
|
|
will = (will_t*)malloc(sizeof(will_t));
|
|
release_assert(will != NULL);
|
|
|
|
gc_total_bytes += sizeof(will_t);
|
|
_get_object(result)->will = will;
|
|
|
|
will->value = value;
|
|
will->finalizer = finalizer;
|
|
will->next = gc_will_list;
|
|
gc_will_list = result;
|
|
|
|
return result;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
|
|
value_t string_to_value(const char *s)
|
|
{
|
|
size_t len = strlen(s);
|
|
value_t v = make_byte_string(len, '\0');
|
|
memcpy(get_byte_string(v)->bytes, s, len);
|
|
return v;
|
|
}
|
|
|
|
char *value_to_string(value_t v)
|
|
{
|
|
byte_string_t *str = get_byte_string(v);
|
|
char *s = (char*)malloc(str->nbytes + 1);
|
|
|
|
memcpy(s, str->bytes, str->nbytes);
|
|
s[str->nbytes] = '\0';
|
|
|
|
return s;
|
|
}
|
|
|
|
int byte_strcmp(value_t s1, value_t s2)
|
|
{
|
|
byte_string_t *bstr1 = get_byte_string(s1);
|
|
byte_string_t *bstr2 = get_byte_string(s2);
|
|
int cmp = memcmp(bstr1->bytes, bstr2->bytes, GC_MIN(bstr1->nbytes, bstr2->nbytes));
|
|
|
|
if (cmp == 0)
|
|
{
|
|
/* Prefix is the same, so compare lengths */
|
|
if (bstr1->nbytes < bstr2->nbytes)
|
|
cmp = -1;
|
|
else if (bstr1->nbytes > bstr2->nbytes)
|
|
cmp = 1;
|
|
}
|
|
|
|
return cmp;
|
|
}
|
|
|
|
value_t get_structure_type(void)
|
|
{
|
|
return structure_type_root.value;
|
|
}
|
|
|
|
value_t make_struct_type(value_t super, fixnum_t nslots, value_t callable)
|
|
{
|
|
gc_root_t super_root;
|
|
gc_root_t callable_root;
|
|
value_t result;
|
|
|
|
register_gc_root(&super_root, super);
|
|
register_gc_root(&callable_root, callable);
|
|
|
|
if (super != FALSE_VALUE)
|
|
{
|
|
release_assert(struct_is_a(super, get_structure_type()));
|
|
release_assert(get_struct(super)->immutable);
|
|
}
|
|
|
|
result = make_struct(get_structure_type());
|
|
|
|
unregister_gc_root(&super_root);
|
|
unregister_gc_root(&callable_root);
|
|
|
|
get_struct(result)->slots[STRUCTURE_SLOT_SUPER] = super;
|
|
get_struct(result)->slots[STRUCTURE_SLOT_NSLOTS] = make_fixnum(nslots);
|
|
get_struct(result)->slots[STRUCTURE_SLOT_CALLABLE] = callable;
|
|
get_struct(result)->immutable = true;
|
|
|
|
return result;
|
|
}
|
|
|
|
bool struct_is_a(value_t value, value_t type)
|
|
{
|
|
value_t tortoise, hare;
|
|
|
|
/* The trivial cases: non-struct and exact match */
|
|
if (!is_struct(value)) return false;
|
|
if (get_struct(value)->type == type) return true;
|
|
|
|
/* Look for type in superclasses; detect cycles using "tortoise and hare" algorithm */
|
|
tortoise = hare = SLOT_VALUE(STRUCTURE, get_struct(value)->type, SUPER);
|
|
|
|
do {
|
|
if (hare == type) return true;
|
|
if (hare == FALSE_VALUE) return false;
|
|
|
|
release_assert(is_struct(hare));
|
|
hare = SLOT_VALUE(STRUCTURE, hare, SUPER);
|
|
|
|
if (hare == type) return true;
|
|
if (hare == FALSE_VALUE) return false;
|
|
|
|
release_assert(is_struct(hare));
|
|
hare = SLOT_VALUE(STRUCTURE, hare, SUPER);
|
|
|
|
/* Tortoise moves ahead one item for every two items covered by hare. */
|
|
/* If there are no cycles in the superclass list, it will always be behind. */
|
|
tortoise = SLOT_VALUE(STRUCTURE, tortoise, SUPER);
|
|
} while (hare != tortoise);
|
|
|
|
/* Tortoise caught up with hare, meaning we detected a cycle. */
|
|
/* If type was in the cycle, it would have been found by now. */
|
|
return false;
|
|
}
|
|
|
|
void register_finalizer(value_t value, value_t finalizer)
|
|
{
|
|
/* Non-objects are never GC'd, so their finalizers will never be invoked. */
|
|
if (is_object(value))
|
|
{
|
|
(void)make_will(value, finalizer);
|
|
}
|
|
}
|
|
|
|
/*************************** Common Collector Code **************************/
|
|
|
|
static void structure_init(void);
|
|
static void mark_roots_live(void);
|
|
static void mark_children_live(value_t parent);
|
|
static void process_weak_boxes(void);
|
|
static void process_wills(void);
|
|
static void update_weak_box_list(void);
|
|
|
|
static void structure_init(void)
|
|
{
|
|
value_t result;
|
|
struct_t *str;
|
|
|
|
/* Instances of this structure describe structures. */
|
|
/* It is both a structure and a structure description, and thus an instance of itself. */
|
|
result = allocate_object(OBJECT_TAG_STRUCT);
|
|
|
|
str = (struct_t*)malloc(STRUCT_BYTES(STRUCTURE_SLOTS));
|
|
release_assert(str != NULL);
|
|
|
|
gc_total_bytes += STRUCT_BYTES(STRUCTURE_SLOTS);
|
|
_get_object(result)->structure = str;
|
|
|
|
str->immutable = true;
|
|
str->type = result;
|
|
str->nslots = STRUCTURE_SLOTS;
|
|
|
|
/* Slot 1: Superclass, if any, or #f */
|
|
str->slots[STRUCTURE_SLOT_SUPER] = FALSE_VALUE;
|
|
|
|
/* Slot 2: Total number of slots (excl. type) */
|
|
str->slots[STRUCTURE_SLOT_NSLOTS] = make_fixnum(STRUCTURE_SLOTS);
|
|
|
|
/* Slot 3: Callable object used as proxy when structure is APPLY'd. */
|
|
/* Can be LAMBDA, callable structure instance, builtin, or FALSE_VALUE. */
|
|
str->slots[STRUCTURE_SLOT_CALLABLE] = FALSE_VALUE;
|
|
|
|
register_gc_root(&structure_type_root, result);
|
|
}
|
|
|
|
void gc_init(void)
|
|
{
|
|
const char *gc_debug_env;
|
|
|
|
memset(object_blocks, 0, sizeof object_blocks);
|
|
|
|
gc_free_list = NIL;
|
|
gc_weak_box_list = NIL;
|
|
gc_will_list = NIL;
|
|
gc_will_active_list = NIL;
|
|
gc_next_live_value = NIL;
|
|
|
|
gc_live_bytes = 0;
|
|
gc_total_bytes = 0;
|
|
|
|
clear_gc_stats();
|
|
|
|
if ((gc_debug_env = getenv("GC_DEBUG")) != NULL)
|
|
{
|
|
if (strcmp(gc_debug_env, "warn") == 0)
|
|
{
|
|
gc_debug_level = GC_DEBUG_LEVEL_WARN;
|
|
}
|
|
else if (strcmp(gc_debug_env, "info") == 0)
|
|
{
|
|
gc_debug_level = GC_DEBUG_LEVEL_INFO;
|
|
}
|
|
else if (strcmp(gc_debug_env, "trace") == 0)
|
|
{
|
|
gc_debug_level = GC_DEBUG_LEVEL_TRACE;
|
|
}
|
|
else
|
|
{
|
|
char *endp;
|
|
long val;
|
|
|
|
val = strtol(gc_debug_env, &endp, 0);
|
|
|
|
if (endp && (endp[0] == '\0'))
|
|
{
|
|
gc_debug_level = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
gc_enabled = true;
|
|
|
|
structure_init();
|
|
}
|
|
|
|
void clear_gc_stats(void)
|
|
{
|
|
gc_stats.passes = 0;
|
|
gc_stats.total_ns = 0;
|
|
gc_stats.peak_ns = 0;
|
|
gc_stats.total_freed = 0;
|
|
gc_stats.peak_allocated = 0;
|
|
}
|
|
|
|
static inline void mark_object_live(value_t value)
|
|
{
|
|
if (is_object(value) && !is_object_live(value))
|
|
{
|
|
set_object_live(value);
|
|
|
|
if ((gc_next_live_value > value) ||
|
|
(gc_next_live_value == NIL))
|
|
{
|
|
gc_next_live_value = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void mark_roots_live(void)
|
|
{
|
|
/* Mark registered GC roots as live */
|
|
for (gc_root_t *root = gc_root_list.next; root != &gc_root_list; root = root->next)
|
|
{
|
|
mark_object_live(root->value);
|
|
}
|
|
|
|
/* Pending wills and associated finalizers are also roots */
|
|
for (value_t will = gc_will_list; !is_nil(will); will = get_will(will)->next)
|
|
{
|
|
mark_object_live(will);
|
|
mark_object_live(get_will(will)->finalizer);
|
|
|
|
/* Processing for values of pending wills occurs separately from the main GC */
|
|
set_object_processed(will);
|
|
}
|
|
|
|
/* Active wills and associated values and finalizers are also roots */
|
|
for (value_t will = gc_will_active_list; !is_nil(will); will = get_will(will)->next)
|
|
{
|
|
mark_object_live(will);
|
|
mark_object_live(get_will(will)->value);
|
|
mark_object_live(get_will(will)->finalizer);
|
|
|
|
/* No further processing is required for active wills */
|
|
set_object_processed(will);
|
|
}
|
|
}
|
|
|
|
static void mark_children_live(value_t value)
|
|
{
|
|
if (is_box(value))
|
|
{
|
|
mark_object_live(get_box(value)->value);
|
|
}
|
|
else if (is_pair(value))
|
|
{
|
|
mark_object_live(get_pair(value)->car);
|
|
mark_object_live(get_pair(value)->cdr);
|
|
}
|
|
else if (is_vector(value))
|
|
{
|
|
vector_t *vec = get_vector(value);
|
|
int i;
|
|
|
|
for (i = 0; i < vec->nelements; ++i)
|
|
mark_object_live(vec->elements[i]);
|
|
}
|
|
else if (is_struct(value))
|
|
{
|
|
struct_t *str = get_struct(value);
|
|
int i;
|
|
|
|
mark_object_live(str->type);
|
|
|
|
for (i = 0; i < str->nslots; ++i)
|
|
mark_object_live(str->slots[i]);
|
|
}
|
|
}
|
|
|
|
static value_t next_live_object(void)
|
|
{
|
|
int blk;
|
|
int idx;
|
|
|
|
if (is_nil(gc_next_live_value))
|
|
return NIL;
|
|
|
|
blk = OBJECT_BLOCK(gc_next_live_value);
|
|
|
|
for (idx = OBJECT_INDEX(gc_next_live_value); idx <= OBJECT_INDEX_MAX; ++idx)
|
|
{
|
|
uint8_t flags = object_blocks[blk].flag_bits[idx];
|
|
|
|
if ((flags & (FLAG_LIVE_BIT | FLAG_PROC_BIT)) == FLAG_LIVE_BIT)
|
|
{
|
|
gc_next_live_value = value_from_index(blk, idx);;
|
|
return gc_next_live_value;;
|
|
}
|
|
}
|
|
|
|
for (++blk; (blk <= OBJECT_BLOCK_MAX) && object_blocks[blk].objects; ++blk)
|
|
{
|
|
for (idx = 0; idx <= OBJECT_INDEX_MAX; ++idx)
|
|
{
|
|
uint8_t flags = object_blocks[blk].flag_bits[idx];
|
|
|
|
if ((flags & (FLAG_LIVE_BIT | FLAG_PROC_BIT)) == FLAG_LIVE_BIT)
|
|
{
|
|
gc_next_live_value = value_from_index(blk, idx);
|
|
return gc_next_live_value;
|
|
}
|
|
}
|
|
}
|
|
|
|
gc_next_live_value = NIL;
|
|
return NIL;
|
|
}
|
|
|
|
static void process_live_objects(void)
|
|
{
|
|
value_t next;
|
|
|
|
while (!is_nil(next = next_live_object()))
|
|
{
|
|
mark_children_live(next);
|
|
set_object_processed(next);
|
|
}
|
|
}
|
|
|
|
static void process_weak_boxes(void)
|
|
{
|
|
value_t wb;
|
|
|
|
for (wb = gc_weak_box_list; !is_nil(wb); wb = get_weak_box(wb)->next)
|
|
{
|
|
weak_box_t *box = get_weak_box(wb);
|
|
|
|
if (!is_object_live(box->value))
|
|
{
|
|
/* The value in the box is an unreachable object; change to #f. */
|
|
|
|
/*
|
|
** NOTE: An object is considered unreachable via weak box when it
|
|
** could be finalized, even though it will be kept alive until any
|
|
** finalizers are removed from the 'active' list. The finalizers
|
|
** may restore the object to a reachable state, in which case it
|
|
** will not be collected--but the weak reference will remain broken.
|
|
**
|
|
** Restoring references to otherwise GC'able objects is not recommended.
|
|
*/
|
|
|
|
box->value = FALSE_VALUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void process_wills(void)
|
|
{
|
|
value_t *will = &gc_will_list;
|
|
|
|
while (!is_nil(*will))
|
|
{
|
|
will_t *w = get_will(*will);
|
|
|
|
if (!is_object_live(w->value))
|
|
{
|
|
/* The will is associated with an unreachable object; activate it. */
|
|
|
|
/* First, ensure the object remains reachable for the finalizer. */
|
|
mark_object_live(w->value);
|
|
|
|
/* Insert the will into the 'active' list. */
|
|
w->next = gc_will_active_list;
|
|
gc_will_active_list = *will;
|
|
|
|
/* Remove the will from the 'pending' list. */
|
|
*will = w->next;
|
|
}
|
|
else
|
|
{
|
|
/* Move on to this will's 'next' pointer */
|
|
will = &w->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void update_weak_box_list(void)
|
|
{
|
|
value_t *wb = &gc_weak_box_list;
|
|
|
|
while (!is_nil(*wb))
|
|
{
|
|
if (!is_object_live(*wb))
|
|
{
|
|
/* Box is no longer reachable; remove it from the list by updating *wb. */
|
|
*wb = get_weak_box(*wb)->next;
|
|
}
|
|
else
|
|
{
|
|
/* Move on to next box */
|
|
wb = &get_weak_box(*wb)->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void free_unreachable_objects(void)
|
|
{
|
|
int blk;
|
|
|
|
for (blk = 0; (blk <= OBJECT_BLOCK_MAX) && object_blocks[blk].objects; ++blk)
|
|
{
|
|
int idx;
|
|
|
|
for (idx = 0; idx <= OBJECT_INDEX_MAX; ++idx)
|
|
{
|
|
value_t value = value_from_index(blk, idx);
|
|
|
|
if (is_object(value) && !is_object_live(value))
|
|
{
|
|
free_object(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void _out_of_memory(void) __attribute__ ((noreturn));
|
|
static void _out_of_memory(void)
|
|
{
|
|
out_of_memory();
|
|
abort();
|
|
}
|
|
|
|
void collect_garbage(void)
|
|
{
|
|
#ifndef NO_STATS
|
|
size_t initial_bytes = gc_total_bytes;
|
|
#ifndef NO_TIMING_STATS
|
|
struct timespec start_time;
|
|
clock_gettime(TIMING_CLOCK, &start_time);
|
|
#endif
|
|
#endif
|
|
|
|
debug_info("Performing garbage collection pass...\n");
|
|
|
|
/* Start with no live/processed values */
|
|
clear_gc_flag_bits();
|
|
gc_next_live_value = NIL;
|
|
|
|
/* Prime the pump */
|
|
mark_roots_live();
|
|
|
|
/* Keep marking children until there are no more live, unprocessed objects. */
|
|
process_live_objects();
|
|
|
|
/* These have to be examined after normal reachability has been determined. */
|
|
process_weak_boxes();
|
|
process_wills();
|
|
|
|
/*
|
|
** Keep marking children until there are no more live, unprocessed objects.
|
|
**
|
|
** This is so that values which are otherwise unreachable, but have
|
|
** finalizers which may be able to reach them, are not collected
|
|
** prematurely. process_wills() marks the value of any will newly placed
|
|
** on the active list as live. Note that these values may be finalized
|
|
** in any order, and that any weak references have already been cleared.
|
|
*/
|
|
process_live_objects();
|
|
|
|
/* Remove any unreachable weak boxes from the weak box list. */
|
|
update_weak_box_list();
|
|
|
|
/* Finally, return any unreachable objects to the free list. */
|
|
free_unreachable_objects();
|
|
|
|
/* Record total "live" allocation after GC. Determines threshold for next GC. */
|
|
gc_live_bytes = gc_total_bytes;
|
|
|
|
debug_info("Finished collection; active set is %ld bytes.\n", (long)gc_live_bytes);
|
|
|
|
#ifndef NO_STATS
|
|
gc_stats.passes += 1;
|
|
|
|
#ifndef NO_TIMING_STATS
|
|
{
|
|
struct timespec end_time;
|
|
nsec_t nsec;
|
|
|
|
clock_gettime(TIMING_CLOCK, &end_time);
|
|
|
|
nsec = (end_time.tv_sec - start_time.tv_sec) * 1000000000LL;
|
|
nsec += (end_time.tv_nsec - start_time.tv_nsec);
|
|
|
|
gc_stats.total_ns += nsec;
|
|
|
|
if (nsec > gc_stats.peak_ns)
|
|
gc_stats.peak_ns = nsec;
|
|
}
|
|
#endif
|
|
|
|
gc_stats.total_freed += (initial_bytes - gc_live_bytes);
|
|
|
|
if (initial_bytes > gc_stats.peak_allocated)
|
|
gc_stats.peak_allocated = initial_bytes;
|
|
#endif
|
|
}
|
|
|
|
bool set_gc_enabled(bool enable)
|
|
{
|
|
bool was_enabled = gc_enabled;
|
|
gc_enabled = enable;
|
|
return was_enabled;
|
|
}
|
|
|
|
bool are_finalizers_pending(void)
|
|
{
|
|
return !is_nil(gc_will_active_list);
|
|
}
|
|
|
|
/* Finalizer can be registered as #f, but value must be an object.
|
|
* Returning with value == #f means there are no more finalizers. */
|
|
void get_next_finalizer(value_t *value, value_t *finalizer)
|
|
{
|
|
assert(value && finalizer);
|
|
|
|
if (is_nil(gc_will_active_list))
|
|
{
|
|
*value = *finalizer = FALSE_VALUE;
|
|
}
|
|
else
|
|
{
|
|
will_t *w = get_will(gc_will_active_list);
|
|
*value = w->value;
|
|
*finalizer = w->finalizer;
|
|
|
|
/* Remove this finalizer from the list -- up to caller to keep values reachable. */
|
|
gc_will_active_list = w->next;
|
|
}
|
|
}
|
|
|
|
void print_backtrace(void)
|
|
{
|
|
#ifdef ENABLE_BACKTRACE
|
|
void *frames[32];
|
|
backtrace(frames, 32);
|
|
backtrace_symbols_fd(frames, 32, 2);
|
|
#endif
|
|
}
|
|
|
|
void _release_assert(const char *str, const char *file, int line)
|
|
{
|
|
fprintf(stderr, "ERROR: Invalid state detected in %s, line %d.\n"
|
|
"Assertion failed: %s\n",
|
|
file, line, str);
|
|
|
|
abort();
|
|
}
|
|
|
|
static void _fprint_value(FILE *f, value_t v, seen_value_t *seen)
|
|
{
|
|
seen_value_t new_seen = { v, seen };
|
|
int depth = 0;
|
|
value_t builtin_name = reverse_lookup_builtin(v);
|
|
|
|
if (is_byte_string(builtin_name))
|
|
{
|
|
fputs("#=", f);
|
|
_fprint_value(f, builtin_name, NULL);
|
|
return;
|
|
}
|
|
|
|
if (is_object(v) && !(is_float(v) || is_builtin_fn(v) || is_byte_string(v)))
|
|
{
|
|
for (seen_value_t *sv = seen; sv; sv = sv->prev)
|
|
{
|
|
if (v == sv->value)
|
|
{
|
|
fputs("#<cycle>", f);
|
|
return;
|
|
}
|
|
|
|
if (++depth >= 3)
|
|
{
|
|
fputs("...", f);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (v == UNDEFINED)
|
|
{
|
|
fputs("#<undefined>", f);
|
|
}
|
|
else if (v == NIL)
|
|
{
|
|
fputs("nil", f);
|
|
}
|
|
else if (v == FALSE_VALUE)
|
|
{
|
|
fputs("#f", f);
|
|
}
|
|
else if (v == TRUE_VALUE)
|
|
{
|
|
fputs("#t", f);
|
|
}
|
|
else if (v == END_PROGRAM)
|
|
{
|
|
fputs("#<endp>", f);
|
|
}
|
|
else if (is_fixnum(v))
|
|
{
|
|
fprintf(f, "%lld", (long long int)get_fixnum(v));
|
|
}
|
|
else if (is_box(v))
|
|
{
|
|
fputs("#&", f);
|
|
_fprint_value(f, get_box(v)->value, &new_seen);
|
|
}
|
|
else if (is_weak_box(v))
|
|
{
|
|
fputs("#W&", f);
|
|
_fprint_value(f, get_weak_box(v)->value, &new_seen);
|
|
}
|
|
else if (is_pair(v))
|
|
{
|
|
fputc('(', f);
|
|
|
|
_fprint_value(f, get_pair(v)->car, &new_seen);
|
|
v = get_pair(v)->cdr;
|
|
|
|
while (is_pair(v))
|
|
{
|
|
fputc(' ', f);
|
|
_fprint_value(f, get_pair(v)->car, &new_seen);
|
|
v = get_pair(v)->cdr;
|
|
}
|
|
|
|
if (v != NIL)
|
|
{
|
|
fputs(" . ", f);
|
|
_fprint_value(f, v, &new_seen);
|
|
}
|
|
|
|
fputc(')', f);
|
|
}
|
|
else if (is_float(v))
|
|
{
|
|
fprintf(f, "%f", (double)get_float(v));
|
|
}
|
|
else if (is_builtin_fn(v))
|
|
{
|
|
fprintf(f, "#<builtin:%p>", get_builtin_fn(v));
|
|
}
|
|
else if (is_vector(v))
|
|
{
|
|
vector_t *vec = get_vector(v);
|
|
|
|
if (vec->immutable)
|
|
fputs("#@", f);
|
|
|
|
fputs("#(", f);
|
|
|
|
for (size_t i = 0; i < vec->nelements; ++i)
|
|
{
|
|
if (i != 0) fputc(' ', f);
|
|
_fprint_value(f, vec->elements[i], &new_seen);
|
|
}
|
|
|
|
fputc(')', f);
|
|
}
|
|
else if (is_byte_string(v))
|
|
{
|
|
byte_string_t *str = get_byte_string(v);
|
|
size_t written = 0;
|
|
|
|
if (str->immutable)
|
|
fputs("#@", f);
|
|
|
|
fputc('"', f);
|
|
|
|
for (size_t i = 0; i < str->nbytes; ++i)
|
|
{
|
|
int ch = str->bytes[i];
|
|
|
|
if (isprint(ch) && (ch != '\\') && (ch != '\"'))
|
|
{
|
|
fputc(str->bytes[i], f);
|
|
++written;
|
|
}
|
|
else
|
|
{
|
|
fprintf(f, "\\x%.2X", (int)str->bytes[i]);
|
|
written += 4;
|
|
}
|
|
|
|
if (written >= 20)
|
|
{
|
|
fputs("...", f);
|
|
break;
|
|
}
|
|
}
|
|
|
|
fputc('"', f);
|
|
}
|
|
else if (is_struct(v))
|
|
{
|
|
struct_t *str = get_struct(v);
|
|
|
|
if (str->immutable)
|
|
fputs("#@", f);
|
|
|
|
fputs("#S(", f);
|
|
_fprint_value(f, str->type, &new_seen);
|
|
|
|
for (size_t i = 0; i < str->nslots; ++i)
|
|
{
|
|
fputc(' ', f);
|
|
_fprint_value(f, str->slots[i], &new_seen);
|
|
}
|
|
|
|
fputc(')', f);
|
|
}
|
|
else
|
|
{
|
|
fprintf(f, "#<unknown:0x%08lx>", (unsigned long)v);
|
|
}
|
|
}
|
|
|
|
void fprint_value(FILE *f, value_t v)
|
|
{
|
|
_fprint_value(f, v, NULL);
|
|
}
|
|
|
|
static double ns2sec(nsec_t ns) __attribute__ ((const));
|
|
static double ns2sec(nsec_t ns)
|
|
{
|
|
return ns / 1.0e9;
|
|
}
|
|
|
|
void fprint_gc_stats(FILE *f)
|
|
{
|
|
if (gc_stats.passes)
|
|
{
|
|
fprintf(f, "GC: %lld bytes freed by %d GCs in %0.6f sec => %0.3f MB/sec.\n"
|
|
"GC: Peak pre-GC memory use was %lld bytes.\n",
|
|
gc_stats.total_freed,
|
|
gc_stats.passes,
|
|
ns2sec(gc_stats.total_ns),
|
|
(gc_stats.total_freed / ns2sec(gc_stats.total_ns)) / (1024*1024),
|
|
gc_stats.peak_allocated);
|
|
}
|
|
else
|
|
{
|
|
fputs("GC: No garbage collection was performed.\n", f);
|
|
}
|
|
}
|
|
|
|
/* vim:set sw=2 expandtab: */
|