all: add the Boehm-Demers-Weiser GC on Linux

This adds support for the well-known Boehm GC. It's significantly faster
than our own naive GC and could be used as an alternative on bigger
systems.

In the future, this GC might also be supported on WebAssembly with some
extra work. Right now it's Linux only (though Windows/MacOS shouldn't be
too difficult to add).
This commit is contained in:
Ayke van Laethem
2023-08-13 23:32:45 +02:00
committed by Ron Evans
parent a4cbe3326e
commit 3a7c25f987
16 changed files with 383 additions and 74 deletions
+6
View File
@@ -534,6 +534,12 @@ func markRoots(start, end uintptr) {
}
}
func markCurrentGoroutineStack(sp uintptr) {
// This could be optimized by only marking the stack area that's currently
// in use.
markRoot(0, sp)
}
// stackOverflow is a flag which is set when the GC scans too deep while marking.
// After it is set, all marked allocations must be re-scanned.
var stackOverflow bool
+172
View File
@@ -0,0 +1,172 @@
//go:build gc.boehm
package runtime
import (
"internal/gclayout"
"internal/reflectlite"
"internal/task"
"unsafe"
)
const needsStaticHeap = false
// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes.
var zeroSizedAlloc uint8
var gcLock task.PMutex
func initHeap() {
libgc_init()
libgc_set_push_other_roots(gcCallbackPtr)
}
var gcCallbackPtr = reflectlite.ValueOf(gcCallback).UnsafePointer()
func gcCallback() {
// Mark the system stack and (if we're on a goroutine stack) also the
// current goroutine stack.
markStack()
findGlobals(func(start, end uintptr) {
libgc_push_all(start, end)
})
}
func markRoots(start, end uintptr) {
libgc_push_all(start, end)
}
func markCurrentGoroutineStack(sp uintptr) {
// Only mark the area of the stack that is currently in use.
// (This doesn't work for other goroutines, but at least it doesn't keep
// more pointers alive than needed on the current stack).
base := libgc_base(sp)
if base == 0 { // && asserts
runtimePanic("goroutine stack not in a heap allocation?")
}
stackBottom := base + libgc_size(base)
libgc_push_all_stack(sp, stackBottom)
}
//go:noinline
func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
if size == 0 {
return unsafe.Pointer(&zeroSizedAlloc)
}
gcLock.Lock()
var ptr unsafe.Pointer
if layout == gclayout.NoPtrs {
// This object is entirely pointer free, for example make([]int, ...).
// Make sure the GC knows this so it doesn't scan the object
// unnecessarily to improve performance.
ptr = libgc_malloc_atomic(size)
// Memory returned from libgc_malloc_atomic has not been zeroed so we
// have to do that manually.
memzero(ptr, size)
} else {
// TODO: bdwgc supports typed allocations, which could be useful to
// implement a mostly-precise GC.
ptr = libgc_malloc(size)
// Memory returned from libgc_malloc has already been zeroed, so nothing
// to do here.
}
gcLock.Unlock()
if ptr == nil {
runtimePanic("gc: out of memory")
}
return ptr
}
func free(ptr unsafe.Pointer) {
libgc_free(ptr)
}
func GC() {
libgc_gcollect()
}
// This should be stack-allocated, but we don't currently have a good way of
// ensuring that happens.
var gcMemStats libgc_prof_stats
func ReadMemStats(m *MemStats) {
gcLock.Lock()
libgc_get_prof_stats(&gcMemStats, unsafe.Sizeof(gcMemStats))
// Fill in MemStats as well as we can, given the information that bdwgc
// provides to us.
m.HeapIdle = uint64(gcMemStats.free_bytes_full - gcMemStats.unmapped_bytes)
m.HeapInuse = uint64(gcMemStats.heapsize_full - gcMemStats.unmapped_bytes)
m.HeapReleased = uint64(gcMemStats.unmapped_bytes)
m.HeapSys = uint64(m.HeapInuse + m.HeapIdle)
m.GCSys = 0 // not provided by bdwgc
m.TotalAlloc = uint64(gcMemStats.allocd_bytes_before_gc + gcMemStats.bytes_allocd_since_gc)
m.Mallocs = 0 // not provided by bdwgc
m.Frees = 0 // not provided by bdwgc
m.Sys = uint64(gcMemStats.obtained_from_os_bytes)
gcLock.Unlock()
}
func setHeapEnd(newHeapEnd uintptr) {
runtimePanic("gc: did not expect setHeapEnd call")
}
func SetFinalizer(obj interface{}, finalizer interface{}) {
// Unimplemented.
// The GC *does* support finalization, so this could be added relatively
// easily I think.
}
//export GC_init
func libgc_init()
//export GC_malloc
func libgc_malloc(uintptr) unsafe.Pointer
//export GC_malloc_atomic
func libgc_malloc_atomic(uintptr) unsafe.Pointer
//export GC_free
func libgc_free(unsafe.Pointer)
//export GC_base
func libgc_base(ptr uintptr) uintptr
//export GC_size
func libgc_size(ptr uintptr) uintptr
//export GC_push_all
func libgc_push_all(bottom, top uintptr)
//export GC_push_all_stack
func libgc_push_all_stack(bottom, top uintptr)
//export GC_gcollect
func libgc_gcollect()
//export GC_get_prof_stats
func libgc_get_prof_stats(*libgc_prof_stats, uintptr) uintptr
//export GC_set_push_other_roots
func libgc_set_push_other_roots(unsafe.Pointer)
type libgc_prof_stats struct {
heapsize_full uintptr
free_bytes_full uintptr
unmapped_bytes uintptr
bytes_allocd_since_gc uintptr
allocd_bytes_before_gc uintptr
non_gc_bytes uintptr
gc_no uintptr
markers_m1 uintptr
bytes_reclaimed_since_gc uintptr
reclaimed_bytes_before_gc uintptr
expl_freed_bytes_since_gc uintptr
obtained_from_os_bytes uintptr
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build (gc.conservative || gc.precise) && (baremetal || tinygo.wasm)
//go:build baremetal || tinygo.wasm
package runtime
+2 -3
View File
@@ -1,4 +1,4 @@
//go:build (gc.conservative || gc.precise) && !tinygo.wasm
//go:build (gc.conservative || gc.precise || gc.boehm) && !tinygo.wasm
package runtime
@@ -33,7 +33,6 @@ func scanstack(sp uintptr) {
markRoots(sp, stackTop)
} else {
// This is a goroutine stack.
// It is an allocation, so scan it as if it were a value in a global.
markRoot(0, sp)
markCurrentGoroutineStack(sp)
}
}