Skip to content

Commit

Permalink
Gc refactor3 rebased (#1195)
Browse files Browse the repository at this point in the history
The condition for requesting collections is replaced by requesting a
collection
if an allocation passes a tripwire. This tripwire is initially set at
MIN_SPACE
and is recalculated after each garbage collation as max(MIN_SPACE, |live
data|).

This avoids a potential issue with the old behavior where collections
were
trigged by allocating in the last 1 MB of previously allocated
semispace, even
if little garbage is being generated, or not collecting frequently
enough if
a period of low garbage generation is followed by a period of high
garbage
generation, affecting cache performance.

Furthermore, garbage collections are only trigged by allocations in
youngspace.
Allocations in oldspace no longer trigger collections since oldspace
collections
are handled during a youngspace collection.
Allocations in alwaysgcspace no longer trigger collections since
collection
of this space is handled outside of the main garbage collector.

Some class arena member functions have the arena_ component removed as
superfluous.
  • Loading branch information
stevenmeker authored Jan 22, 2025
1 parent c2e8094 commit c55f039
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 67 deletions.
79 changes: 28 additions & 51 deletions include/runtime/arena.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,22 @@ extern "C" {

size_t const HYPERBLOCK_SIZE = (size_t)BLOCK_SIZE * 1024 * 1024;

// After a garbage collect we change the tripwire to the amount of non-garbage times
// this factor, so we do a decent amount of allocations between collections even
// when there is very little garbage
size_t const EXPAND_FACTOR = 2;

// We don't consider collecting garbage until at least this amount of space has
// been allocated, to avoid collections near startup when there is little garbage.
size_t const MIN_SPACE = 1024 * 1024;

// An arena can be used to allocate objects that can then be deallocated all at
// once.
class arena {
public:
arena(char id)
: allocation_semispace_id(id) { }
arena(char id, bool trigger_collection)
: allocation_semispace_id(id)
, trigger_collection(trigger_collection) { }

~arena() {
if (current_addr_ptr)
Expand All @@ -36,17 +46,17 @@ class arena {

// Returns the address of the first byte that belongs in the given arena.
// Returns nullptr if nothing has been allocated ever in that arena.
char *arena_start_ptr() const { return current_addr_ptr; }
char *start_ptr() const { return current_addr_ptr; }

// Returns a pointer to a location holding the address of last allocated
// byte in the given arena plus 1.
// This address is nullptr if nothing has been allocated ever in that arena.
char *arena_end_ptr() { return allocation_ptr; }
char *end_ptr() { return allocation_ptr; }

// Clears the current allocation space by setting its start back to its first
// block. It is used during garbage collection to effectively collect all of the
// arena. Resets the tripwire.
void arena_clear();
// arena.
void arena_clear() { allocation_ptr = current_addr_ptr; }

// Resizes the last allocation.
// Returns the address of the byte following the last newly allocated byte.
Expand All @@ -67,6 +77,14 @@ class arena {
// It is used before garbage collection.
void arena_swap_and_clear();

// Decide how much space to use in arena before setting the flag for a collection.
// If an arena is going to request collections, updating this at the end of a
// collection is mandatory.
void update_tripwire() {
size_t space = EXPAND_FACTOR * (allocation_ptr - current_addr_ptr);
tripwire = current_addr_ptr + ((space < MIN_SPACE) ? MIN_SPACE : space);
}

// Given two pointers to objects allocated in the same arena, return the number
// of bytes they are apart. Undefined behavior will result if the pointers
// don't belong to the same arena
Expand All @@ -92,20 +110,6 @@ class arena {
static char get_arena_semispace_id_of_object(void *ptr);

private:
//
// We update the number of 1MB blocks actually written to, only when we need this value,
// or before a garbage collection rather than trying to determine when we write to a fresh block.
//
void update_num_blocks() const {
//
// Calculate how many 1M blocks of the current arena we used.
//
size_t num_used_blocks
= (allocation_ptr - current_addr_ptr - 1) / BLOCK_SIZE + 1;
if (num_used_blocks > num_blocks)
num_blocks = num_used_blocks;
}

void initialize_semispace();
//
// Current semispace where allocations are being made.
Expand All @@ -114,16 +118,13 @@ class arena {
char *allocation_ptr
= nullptr; // next available location in current semispace
char *tripwire = nullptr; // allocating past this triggers slow allocation
mutable size_t num_blocks
= 0; // notional number of BLOCK_SIZE blocks in current semispace
char allocation_semispace_id; // id of current semispace
bool const trigger_collection; // request collections?
//
// Semispace where allocations will be made during and after garbage collect.
//
char *collection_addr_ptr
= nullptr; // pointer to start of collection address space
size_t num_collection_blocks
= 0; // notional number of BLOCK_SIZE blocks in collection semispace
};

inline char arena::get_arena_semispace_id_of_object(void *ptr) {
Expand All @@ -139,10 +140,6 @@ inline char arena::get_arena_semispace_id_of_object(void *ptr) {
return *reinterpret_cast<char *>(end_address);
}

// Macro to define a new arena with the given ID. Supports IDs ranging from 0 to
// 127.
#define REGISTER_ARENA(name, id) thread_local arena name(id)

#ifdef __MACH__
//
// thread_local disabled for Apple
Expand Down Expand Up @@ -183,32 +180,12 @@ inline void *arena::kore_arena_alloc(size_t requested) {
return result;
}

inline void arena::arena_clear() {
//
// We set the allocation pointer to the first available address.
//
allocation_ptr = arena_start_ptr();
//
// If the number of blocks we've touched is >= threshold, we want to trigger
// a garbage collection if we get within 1 block of the end of this area.
// Otherwise we only want to generate a garbage collect if we allocate off the
// end of this area.
//
tripwire = current_addr_ptr
+ (num_blocks - (num_blocks >= get_gc_threshold())) * BLOCK_SIZE;
}

inline void arena::arena_swap_and_clear() {
update_num_blocks(); // so we save the correct number of touched blocks
std::swap(current_addr_ptr, collection_addr_ptr);
std::swap(num_blocks, num_collection_blocks);
allocation_semispace_id = ~allocation_semispace_id;
if (current_addr_ptr == nullptr) {
//
// The other semispace hasn't be initialized yet.
//
initialize_semispace();
} else
if (current_addr_ptr == nullptr)
initialize_semispace(); // not yet initialized
else
arena_clear();
}
}
Expand Down
8 changes: 4 additions & 4 deletions runtime/alloc/arena.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ void arena::initialize_semispace() {
current_addr_ptr[HYPERBLOCK_SIZE - 1] = allocation_semispace_id;
allocation_ptr = current_addr_ptr;
//
// We set the tripwire for this space so we get trigger a garbage collection when we pass BLOCK_SIZE of memory
// allocated from this space.
// If we're set to trigger garbage collections, set the tripwire for MIN_SPACE of allocations otherwise
// set it out-of-bounds (but still legal for comparison).
//
tripwire = current_addr_ptr + BLOCK_SIZE;
num_blocks = 1;
tripwire
= current_addr_ptr + (trigger_collection ? MIN_SPACE : HYPERBLOCK_SIZE);
}
13 changes: 7 additions & 6 deletions runtime/collect/collect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ char *arena::evacuate(char *scan_ptr) {
migrate_child(curr_block, layout_data->args, i, false);
}
}
return move_ptr(scan_ptr, get_size(hdr, layout_int), arena_end_ptr());
return move_ptr(scan_ptr, get_size(hdr, layout_int), end_ptr());
}

// Contains the decision logic for collecting the old generation.
Expand Down Expand Up @@ -295,21 +295,21 @@ void kore_collect(
if (!last_alloc_ptr) {
last_alloc_ptr = youngspace_ptr();
}
char *current_alloc_ptr = youngspace.arena_end_ptr();
char *current_alloc_ptr = youngspace.end_ptr();
#endif
kore_alloc_swap(collect_old);
#ifdef GC_DBG
for (int i = 0; i < 2048; i++) {
numBytesLiveAtCollection[i] = 0;
}
#endif
char *previous_oldspace_alloc_ptr = oldspace.arena_end_ptr();
char *previous_oldspace_alloc_ptr = oldspace.end_ptr();
for (int i = 0; i < nroots; i++) {
migrate_root(roots, type_info, i);
}
migrate_static_roots();
char *scan_ptr = youngspace_ptr();
if (scan_ptr != youngspace.arena_end_ptr()) {
if (scan_ptr != youngspace.end_ptr()) {
MEM_LOG("Evacuating young generation\n");
while (scan_ptr) {
scan_ptr = youngspace.evacuate(scan_ptr);
Expand All @@ -320,7 +320,7 @@ void kore_collect(
} else {
scan_ptr = previous_oldspace_alloc_ptr;
}
if (scan_ptr != oldspace.arena_end_ptr()) {
if (scan_ptr != oldspace.end_ptr()) {
MEM_LOG("Evacuating old generation\n");
while (scan_ptr) {
scan_ptr = oldspace.evacuate(scan_ptr);
Expand All @@ -331,12 +331,13 @@ void kore_collect(
= arena::ptr_diff(current_alloc_ptr, last_alloc_ptr);
assert(numBytesAllocedSinceLastCollection >= 0);
fwrite(&numBytesAllocedSinceLastCollection, sizeof(ssize_t), 1, stderr);
last_alloc_ptr = youngspace.arena_end_ptr();
last_alloc_ptr = youngspace.end_ptr();
fwrite(
numBytesLiveAtCollection, sizeof(numBytesLiveAtCollection[0]),
sizeof(numBytesLiveAtCollection) / sizeof(numBytesLiveAtCollection[0]),
stderr);
#endif
youngspace.update_tripwire();
MEM_LOG("Finishing garbage collection\n");
is_gc = false;
}
Expand Down
22 changes: 16 additions & 6 deletions runtime/lto/alloc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,26 @@

extern "C" {

REGISTER_ARENA(youngspace, YOUNGSPACE_ID);
REGISTER_ARENA(oldspace, OLDSPACE_ID);
REGISTER_ARENA(alwaysgcspace, ALWAYSGCSPACE_ID);
// class arena supports ID from 0 to 127

// New data in allocated in the youngspace, which requests a
// collection when is gets too full.
thread_local arena youngspace(YOUNGSPACE_ID, true);

// Data that is old enough is migrated to the oldspace. The
// migrated data is always live at this point so it never
// requests a collection.
thread_local arena oldspace(OLDSPACE_ID, false);

// Temporary data is doesn't use the garbage collector.
thread_local arena alwaysgcspace(ALWAYSGCSPACE_ID, false);

char *youngspace_ptr() {
return youngspace.arena_start_ptr();
return youngspace.start_ptr();
}

char *oldspace_ptr() {
return oldspace.arena_start_ptr();
return oldspace.start_ptr();
}

char youngspace_collection_id() {
Expand Down Expand Up @@ -73,7 +83,7 @@ kore_resize_last_alloc(void *oldptr, size_t newrequest, size_t last_size) {
newrequest = (newrequest + 7) & ~7;
last_size = (last_size + 7) & ~7;

if (oldptr != youngspace.arena_end_ptr() - last_size) {
if (oldptr != youngspace.end_ptr() - last_size) {
MEM_LOG(
"May only reallocate last allocation. Tried to reallocate %p to %zd\n",
oldptr, newrequest);
Expand Down

0 comments on commit c55f039

Please sign in to comment.