From c55f039005bb2c6d97f5dae426040d70720b4ebe Mon Sep 17 00:00:00 2001 From: Steven Eker Date: Wed, 22 Jan 2025 09:01:32 -0800 Subject: [PATCH] Gc refactor3 rebased (#1195) 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. --- include/runtime/arena.h | 79 +++++++++++++------------------------ runtime/alloc/arena.cpp | 8 ++-- runtime/collect/collect.cpp | 13 +++--- runtime/lto/alloc.cpp | 22 ++++++++--- 4 files changed, 55 insertions(+), 67 deletions(-) diff --git a/include/runtime/arena.h b/include/runtime/arena.h index 826db686c..ea081766a 100644 --- a/include/runtime/arena.h +++ b/include/runtime/arena.h @@ -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) @@ -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. @@ -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 @@ -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. @@ -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) { @@ -139,10 +140,6 @@ inline char arena::get_arena_semispace_id_of_object(void *ptr) { return *reinterpret_cast(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 @@ -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(); } } diff --git a/runtime/alloc/arena.cpp b/runtime/alloc/arena.cpp index fbfd7e527..85171aed4 100644 --- a/runtime/alloc/arena.cpp +++ b/runtime/alloc/arena.cpp @@ -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); } diff --git a/runtime/collect/collect.cpp b/runtime/collect/collect.cpp index f49123950..981b29955 100644 --- a/runtime/collect/collect.cpp +++ b/runtime/collect/collect.cpp @@ -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. @@ -295,7 +295,7 @@ 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 @@ -303,13 +303,13 @@ void kore_collect( 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); @@ -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); @@ -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; } diff --git a/runtime/lto/alloc.cpp b/runtime/lto/alloc.cpp index aaafc932d..e0d7a010b 100644 --- a/runtime/lto/alloc.cpp +++ b/runtime/lto/alloc.cpp @@ -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() { @@ -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);