github.com/wasilibs/nottinygc@v0.7.2-0.20240312114022-d59c9478ef51/gc.go (about)

     1  // Copyright wasilibs authors
     2  // SPDX-License-Identifier: MIT
     3  
     4  //go:build gc.custom
     5  
     6  package nottinygc
     7  
     8  import (
     9  	"math/bits"
    10  	"runtime"
    11  	"unsafe"
    12  )
    13  
    14  /*
    15  #include <stddef.h>
    16  
    17  void* GC_malloc(unsigned int size);
    18  void* GC_malloc_atomic(unsigned int size);
    19  void* GC_malloc_ignore_off_page(unsigned int size);
    20  void* GC_malloc_explicitly_typed(unsigned int size, unsigned int gc_descr);
    21  void* GC_malloc_explicitly_typed_ignore_off_page(unsigned int size, unsigned int gc_descr);
    22  void* GC_calloc_explicitly_typed(unsigned int nelements, unsigned int element_size, unsigned int gc_descr);
    23  unsigned int GC_make_descriptor(void* bm, unsigned int len);
    24  void GC_free(void* ptr);
    25  void GC_gcollect();
    26  void GC_set_on_collection_event(void* f);
    27  
    28  size_t GC_get_gc_no();
    29  void GC_get_heap_usage_safe(size_t* heap_size, size_t* free_bytes, size_t* unmapped_bytes, size_t* bytesSinceGC, size_t* totalBytes);
    30  size_t GC_get_obtained_from_os_bytes();
    31  void mi_process_info(size_t *elapsed_msecs, size_t *user_msecs, size_t *system_msecs, size_t *current_rss, size_t *peak_rss, size_t *current_commit, size_t *peak_commit, size_t *page_faults);
    32  
    33  void GC_ignore_warn_proc(char* msg, unsigned int arg);
    34  void GC_set_warn_proc(void* p);
    35  
    36  void onCollectionEvent();
    37  */
    38  import "C"
    39  
    40  const (
    41  	gcEventStart = 0
    42  	bigObjSize   = 100 * 1024
    43  )
    44  
    45  const (
    46  	gcDsBitmap = uintptr(1)
    47  )
    48  
    49  var descriptorCache intMap
    50  
    51  //export onCollectionEvent
    52  func onCollectionEvent(eventType uint32) {
    53  	switch eventType {
    54  	case gcEventStart:
    55  		markStack()
    56  	}
    57  }
    58  
    59  // Initialize the memory allocator.
    60  //
    61  //go:linkname initHeap runtime.initHeap
    62  func initHeap() {
    63  	descriptorCache = newIntMap()
    64  
    65  	C.GC_set_on_collection_event(C.onCollectionEvent)
    66  	// We avoid overhead in calling GC_make_descriptor on every allocation by implementing
    67  	// the bitmap computation in Go, but we need to call it at least once to initialize
    68  	// typed GC itself.
    69  	C.GC_make_descriptor(nil, 0)
    70  	C.GC_set_warn_proc(C.GC_ignore_warn_proc)
    71  }
    72  
    73  // alloc tries to find some free space on the heap, possibly doing a garbage
    74  // collection cycle if needed. If no space is free, it panics.
    75  //
    76  //go:linkname alloc runtime.alloc
    77  func alloc(size uintptr, layoutPtr unsafe.Pointer) unsafe.Pointer {
    78  	var buf unsafe.Pointer
    79  
    80  	layout := uintptr(layoutPtr)
    81  	if layout&1 != 0 {
    82  		// Layout is stored directly in the integer value.
    83  		// Determine format of bitfields in the integer.
    84  		const layoutBits = uint64(unsafe.Sizeof(layout) * 8)
    85  		var sizeFieldBits uint64
    86  		switch layoutBits { // note: this switch should be resolved at compile time
    87  		case 16:
    88  			sizeFieldBits = 4
    89  		case 32:
    90  			sizeFieldBits = 5
    91  		case 64:
    92  			sizeFieldBits = 6
    93  		default:
    94  			panic("unknown pointer size")
    95  		}
    96  		layoutSz := (layout >> 1) & (1<<sizeFieldBits - 1)
    97  		layoutBm := layout >> (1 + sizeFieldBits)
    98  		buf = allocSmall(size, layoutSz, layoutBm)
    99  	} else if layoutPtr == nil {
   100  		// Unknown layout, assume all pointers.
   101  		if size >= bigObjSize {
   102  			buf = C.GC_malloc_ignore_off_page(C.uint(size))
   103  		} else {
   104  			buf = C.GC_malloc(C.uint(size))
   105  		}
   106  	} else {
   107  		buf = allocLarge(size, layoutPtr)
   108  	}
   109  	if buf == nil {
   110  		panic("out of memory")
   111  	}
   112  	return buf
   113  }
   114  
   115  func allocSmall(allocSz uintptr, layoutSz uintptr, layoutBm uintptr) unsafe.Pointer {
   116  	desc := gcDescr(layoutBm)
   117  	return allocTyped(allocSz, layoutSz, desc)
   118  }
   119  
   120  func allocLarge(allocSz uintptr, layoutPtr unsafe.Pointer) unsafe.Pointer {
   121  	layoutSz := *(*uintptr)(layoutPtr)
   122  	desc, ok := descriptorCache.get(uintptr(layoutPtr))
   123  	if !ok {
   124  		bm := newBitmap(layoutSz)
   125  		defer bm.free()
   126  
   127  		bitsPtr := unsafe.Add(layoutPtr, unsafe.Sizeof(uintptr(0)))
   128  		for i := uintptr(0); i < layoutSz; i++ {
   129  			if (*(*uint8)(unsafe.Add(bitsPtr, i/8))>>(i%8))&1 != 0 {
   130  				bm.set(i)
   131  			}
   132  		}
   133  		desc = uintptr(C.GC_make_descriptor(unsafe.Pointer(&bm.words[0]), C.uint(layoutSz)))
   134  		descriptorCache.put(uintptr(layoutPtr), desc)
   135  	}
   136  
   137  	return allocTyped(allocSz, layoutSz, desc)
   138  }
   139  
   140  func allocTyped(allocSz uintptr, layoutSz uintptr, desc uintptr) unsafe.Pointer {
   141  	itemSz := layoutSz * unsafe.Sizeof(uintptr(0))
   142  	if desc == 0 || itemSz == allocSz {
   143  		// A bit unsure what the difference is, but it is recommended by bdwgc and seems to make a big
   144  		// difference in some apps.
   145  		// https://github.com/ivmai/bdwgc/blob/master/README.md#the-c-interface-to-the-allocator
   146  		if allocSz >= bigObjSize {
   147  			return C.GC_malloc_explicitly_typed_ignore_off_page(C.uint(allocSz), C.uint(desc))
   148  		}
   149  		return C.GC_malloc_explicitly_typed(C.uint(allocSz), C.uint(desc))
   150  	}
   151  	numItems := allocSz / itemSz
   152  	return C.GC_calloc_explicitly_typed(C.uint(numItems), C.uint(itemSz), C.uint(desc))
   153  }
   154  
   155  // Reimplementation of the simple bitmap case from bdwgc
   156  // https://github.com/ivmai/bdwgc/blob/806537be2dec4f49056cb2fe091ac7f7d78728a8/typd_mlc.c#L204
   157  func gcDescr(layoutBm uintptr) uintptr {
   158  	if layoutBm == 0 {
   159  		return 0 // no pointers
   160  	}
   161  
   162  	// reversebits processes all bits but is branchless, unlike a looping version so appears
   163  	// to perform a little better.
   164  	return uintptr(bits.Reverse32(uint32(layoutBm))) | gcDsBitmap
   165  }
   166  
   167  //go:linkname free runtime.free
   168  func free(ptr unsafe.Pointer) {
   169  	C.GC_free(ptr)
   170  }
   171  
   172  //go:linkname markRoots runtime.markRoots
   173  func markRoots(start, end uintptr) {
   174  	// Roots are already registered in bdwgc so we have nothing to do here.
   175  }
   176  
   177  //go:linkname markStack runtime.markStack
   178  func markStack()
   179  
   180  // GC performs a garbage collection cycle.
   181  //
   182  //go:linkname GC runtime.GC
   183  func GC() {
   184  	C.GC_gcollect()
   185  }
   186  
   187  //go:linkname ReadMemStats runtime.ReadMemStats
   188  func ReadMemStats(ms *runtime.MemStats) {
   189  	var heapSize, freeBytes, unmappedBytes, bytesSinceGC, totalBytes C.size_t
   190  	C.GC_get_heap_usage_safe(&heapSize, &freeBytes, &unmappedBytes, &bytesSinceGC, &totalBytes)
   191  
   192  	var peakRSS C.size_t
   193  	C.mi_process_info(nil, nil, nil, nil, &peakRSS, nil, nil, nil)
   194  
   195  	gcOSBytes := C.GC_get_obtained_from_os_bytes()
   196  
   197  	ms.Sys = uint64(peakRSS + gcOSBytes)
   198  	ms.HeapSys = uint64(heapSize)
   199  	ms.HeapIdle = uint64(freeBytes)
   200  	ms.HeapReleased = uint64(unmappedBytes)
   201  	ms.TotalAlloc = uint64(totalBytes)
   202  }