mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-22 13:29:01 +00:00
runtime (gc_blocks.go): use a linked stack to scan marked objects
The blocks GC originally used a fixed-size stack to hold objects to scan. When this stack overflowed, the GC would fully rescan all marked objects. This could cause the GC to degrade to O(n^2) when scanning large linked data structures. Instead of using a fixed-size stack, we now add a pointer field to the start of each object. This pointer field is used to implement an unbounded linked stack. This also consolidates the heap object scanning into one place, which simplifies the process. This comes at the cost of introducing a pointer field to the start of the object, plus the cost of aligning the result. This translates to: - 16 bytes of overhead on x86/arm64 with the conservative collector - 0 bytes of overhead on x86/arm64 with the precise collector (the layout field cost gets aligned up to 16 bytes anyway) - 8 bytes of overhead on other 64-bit systems - 4 bytes of overhead on 32-bit systems - 2 bytes of overhead on AVR
This commit is contained in:
@@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) {
|
|||||||
// This is a small number of very diverse targets that we want to test.
|
// This is a small number of very diverse targets that we want to test.
|
||||||
tests := []sizeTest{
|
tests := []sizeTest{
|
||||||
// microcontrollers
|
// microcontrollers
|
||||||
{"hifive1b", "examples/echo", 3896, 280, 0, 2268},
|
{"hifive1b", "examples/echo", 3756, 280, 0, 2268},
|
||||||
{"microbit", "examples/serial", 2860, 360, 8, 2272},
|
{"microbit", "examples/serial", 2756, 340, 8, 2272},
|
||||||
{"wioterminal", "examples/pininterrupt", 7361, 1491, 116, 6912},
|
{"wioterminal", "examples/pininterrupt", 7297, 1491, 116, 6912},
|
||||||
|
|
||||||
// TODO: also check wasm. Right now this is difficult, because
|
// TODO: also check wasm. Right now this is difficult, because
|
||||||
// wasm binaries are run through wasm-opt and therefore the
|
// wasm binaries are run through wasm-opt and therefore the
|
||||||
|
|||||||
+57
-74
@@ -46,11 +46,11 @@ const (
|
|||||||
bytesPerBlock = wordsPerBlock * unsafe.Sizeof(heapStart)
|
bytesPerBlock = wordsPerBlock * unsafe.Sizeof(heapStart)
|
||||||
stateBits = 2 // how many bits a block state takes (see blockState type)
|
stateBits = 2 // how many bits a block state takes (see blockState type)
|
||||||
blocksPerStateByte = 8 / stateBits
|
blocksPerStateByte = 8 / stateBits
|
||||||
markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
metadataStart unsafe.Pointer // pointer to the start of the heap metadata
|
metadataStart unsafe.Pointer // pointer to the start of the heap metadata
|
||||||
|
scanList *objHeader // scanList is a singly linked list of heap objects that have been marked but not scanned
|
||||||
nextAlloc gcBlock // the next block that should be tried by the allocator
|
nextAlloc gcBlock // the next block that should be tried by the allocator
|
||||||
endBlock gcBlock // the block just past the end of the available space
|
endBlock gcBlock // the block just past the end of the available space
|
||||||
gcTotalAlloc uint64 // total number of bytes allocated
|
gcTotalAlloc uint64 // total number of bytes allocated
|
||||||
@@ -225,6 +225,15 @@ func (b gcBlock) unmark() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// objHeader is a structure prepended to every heap object to hold metadata.
|
||||||
|
type objHeader struct {
|
||||||
|
// next is the next object to scan after this.
|
||||||
|
next *objHeader
|
||||||
|
|
||||||
|
// layout holds the layout bitmap used to find pointers in the object.
|
||||||
|
layout gcLayout
|
||||||
|
}
|
||||||
|
|
||||||
func isOnHeap(ptr uintptr) bool {
|
func isOnHeap(ptr uintptr) bool {
|
||||||
return ptr >= heapStart && ptr < uintptr(metadataStart)
|
return ptr >= heapStart && ptr < uintptr(metadataStart)
|
||||||
}
|
}
|
||||||
@@ -315,13 +324,10 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
|||||||
runtimePanicAt(returnAddress(0), "heap alloc in interrupt")
|
runtimePanicAt(returnAddress(0), "heap alloc in interrupt")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Round the size up to a multiple of blocks.
|
// Round the size up to a multiple of blocks, adding space for the header.
|
||||||
rawSize := size
|
rawSize := size
|
||||||
|
size += align(unsafe.Sizeof(objHeader{}))
|
||||||
size += bytesPerBlock - 1
|
size += bytesPerBlock - 1
|
||||||
if preciseHeap {
|
|
||||||
// Add space for the layout.
|
|
||||||
size += align(unsafe.Sizeof(layout))
|
|
||||||
}
|
|
||||||
if size < rawSize {
|
if size < rawSize {
|
||||||
// The size overflowed.
|
// The size overflowed.
|
||||||
runtimePanicAt(returnAddress(0), "out of memory")
|
runtimePanicAt(returnAddress(0), "out of memory")
|
||||||
@@ -414,20 +420,18 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
|||||||
i.setState(blockStateTail)
|
i.setState(blockStateTail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the object header.
|
||||||
|
pointer := thisAlloc.pointer()
|
||||||
|
header := (*objHeader)(pointer)
|
||||||
|
header.layout = parseGCLayout(layout)
|
||||||
|
|
||||||
// We've claimed this allocation, now we can unlock the heap.
|
// We've claimed this allocation, now we can unlock the heap.
|
||||||
gcLock.Unlock()
|
gcLock.Unlock()
|
||||||
|
|
||||||
// Return a pointer to this allocation.
|
// Return a pointer to this allocation.
|
||||||
pointer := thisAlloc.pointer()
|
add := align(unsafe.Sizeof(objHeader{}))
|
||||||
if preciseHeap {
|
|
||||||
// Store the object layout at the start of the object.
|
|
||||||
// TODO: this wastes a little bit of space on systems with
|
|
||||||
// larger-than-pointer alignment requirements.
|
|
||||||
*(*unsafe.Pointer)(pointer) = layout
|
|
||||||
add := align(unsafe.Sizeof(layout))
|
|
||||||
pointer = unsafe.Add(pointer, add)
|
pointer = unsafe.Add(pointer, add)
|
||||||
size -= add
|
size -= add
|
||||||
}
|
|
||||||
memzero(pointer, size)
|
memzero(pointer, size)
|
||||||
return pointer
|
return pointer
|
||||||
}
|
}
|
||||||
@@ -562,42 +566,33 @@ func markCurrentGoroutineStack(sp uintptr) {
|
|||||||
markRoot(0, sp)
|
markRoot(0, sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stackOverflow is a flag which is set when the GC scans too deep while marking.
|
// finishMark finishes the marking process by scanning all heap objects on scanList.
|
||||||
// After it is set, all marked allocations must be re-scanned.
|
func finishMark() {
|
||||||
var stackOverflow bool
|
for {
|
||||||
|
// Remove an object from the scan list.
|
||||||
// startMark starts the marking process on a root and all of its children.
|
obj := scanList
|
||||||
func startMark(root gcBlock) {
|
if obj == nil {
|
||||||
var stack [markStackSize]gcBlock
|
return
|
||||||
stack[0] = root
|
|
||||||
root.setState(blockStateMark)
|
|
||||||
stackLen := 1
|
|
||||||
for stackLen > 0 {
|
|
||||||
// Pop a block off of the stack.
|
|
||||||
stackLen--
|
|
||||||
block := stack[stackLen]
|
|
||||||
if gcDebug {
|
|
||||||
println("stack popped, remaining stack:", stackLen)
|
|
||||||
}
|
}
|
||||||
|
scanList = obj.next
|
||||||
|
|
||||||
// Scan all pointers inside the block.
|
// Create a scanner with the object layout.
|
||||||
scanner := newGCObjectScanner(block)
|
scanner := obj.layout.scanner()
|
||||||
if scanner.pointerFree() {
|
if scanner.pointerFree() {
|
||||||
// This object doesn't contain any pointers.
|
// This object doesn't contain any pointers.
|
||||||
// This is a fast path for objects like make([]int, 4096).
|
// This is a fast path for objects like make([]int, 4096).
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
start, end := block.address(), block.findNext().address()
|
|
||||||
if preciseHeap {
|
// Scan all pointers in the object.
|
||||||
// The first word of the object is just the pointer layout value.
|
start := uintptr(unsafe.Pointer(obj)) + align(unsafe.Sizeof(objHeader{}))
|
||||||
// Skip it.
|
end := blockFromAddr(uintptr(unsafe.Pointer(obj))).findNext().address()
|
||||||
start += align(unsafe.Sizeof(uintptr(0)))
|
|
||||||
}
|
|
||||||
for addr := start; addr != end; addr += unsafe.Alignof(addr) {
|
for addr := start; addr != end; addr += unsafe.Alignof(addr) {
|
||||||
// Load the word.
|
// Load the word.
|
||||||
word := *(*uintptr)(unsafe.Pointer(addr))
|
word := *(*uintptr)(unsafe.Pointer(addr))
|
||||||
|
|
||||||
if !scanner.nextIsPointer(word, root.address(), addr) {
|
if !scanner.nextIsPointer(word, uintptr(unsafe.Pointer(obj)), addr) {
|
||||||
// Not a heap pointer.
|
// Not a heap pointer.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -628,44 +623,24 @@ func startMark(root gcBlock) {
|
|||||||
}
|
}
|
||||||
referencedBlock.setState(blockStateMark)
|
referencedBlock.setState(blockStateMark)
|
||||||
|
|
||||||
if stackLen == len(stack) {
|
// Add the object to the scan list.
|
||||||
// The stack is full.
|
header := (*objHeader)(referencedBlock.pointer())
|
||||||
// It is necessary to rescan all marked blocks once we are done.
|
header.next = scanList
|
||||||
stackOverflow = true
|
scanList = header
|
||||||
if gcDebug {
|
|
||||||
println("gc stack overflowed")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the pointer onto the stack to be scanned later.
|
|
||||||
stack[stackLen] = referencedBlock
|
|
||||||
stackLen++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finishMark finishes the marking process by processing all stack overflows.
|
|
||||||
func finishMark() {
|
|
||||||
for stackOverflow {
|
|
||||||
// Re-mark all blocks.
|
|
||||||
stackOverflow = false
|
|
||||||
for block := gcBlock(0); block < endBlock; block++ {
|
|
||||||
if block.state() != blockStateMark {
|
|
||||||
// Block is not marked, so we do not need to rescan it.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-mark the block.
|
|
||||||
startMark(block)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mark a GC root at the address addr.
|
// mark a GC root at the address addr.
|
||||||
func markRoot(addr, root uintptr) {
|
func markRoot(addr, root uintptr) {
|
||||||
if isOnHeap(root) {
|
// Find the heap block corresponding to the root.
|
||||||
|
if !isOnHeap(root) {
|
||||||
|
// This is not a heap pointer.
|
||||||
|
return
|
||||||
|
}
|
||||||
block := blockFromAddr(root)
|
block := blockFromAddr(root)
|
||||||
|
|
||||||
|
// Find the head of the corresponding object.
|
||||||
if block.state() == blockStateFree {
|
if block.state() == blockStateFree {
|
||||||
// The to-be-marked object doesn't actually exist.
|
// The to-be-marked object doesn't actually exist.
|
||||||
// This could either be a dangling pointer (oops!) but most likely
|
// This could either be a dangling pointer (oops!) but most likely
|
||||||
@@ -673,13 +648,21 @@ func markRoot(addr, root uintptr) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
head := block.findHead()
|
head := block.findHead()
|
||||||
if head.state() != blockStateMark {
|
|
||||||
|
// Mark the object.
|
||||||
|
if head.state() == blockStateMark {
|
||||||
|
// This object is already marked.
|
||||||
|
return
|
||||||
|
}
|
||||||
if gcDebug {
|
if gcDebug {
|
||||||
println("found unmarked pointer", root, "at address", addr)
|
println("found unmarked pointer", root, "at address", addr)
|
||||||
}
|
}
|
||||||
startMark(head)
|
head.setState(blockStateMark)
|
||||||
}
|
|
||||||
}
|
// Add the object to the scan list.
|
||||||
|
header := (*objHeader)(head.pointer())
|
||||||
|
header.next = scanList
|
||||||
|
scanList = header
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sweep goes through all memory and frees unmarked memory.
|
// Sweep goes through all memory and frees unmarked memory.
|
||||||
|
|||||||
@@ -6,15 +6,27 @@
|
|||||||
|
|
||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
const preciseHeap = false
|
import "unsafe"
|
||||||
|
|
||||||
type gcObjectScanner struct {
|
// gcLayout tracks pointer locations in a heap object.
|
||||||
|
// The conservative GC treats all locations as potential pointers, so this doesn't need to store anything.
|
||||||
|
type gcLayout struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGCObjectScanner(block gcBlock) gcObjectScanner {
|
// parseGCLayout stores the layout information passed to alloc into a gcLayout value.
|
||||||
|
// The conservative GC discards this information.
|
||||||
|
func parseGCLayout(layout unsafe.Pointer) gcLayout {
|
||||||
|
return gcLayout{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanner creates a gcObjectScanner with this layout.
|
||||||
|
func (l gcLayout) scanner() gcObjectScanner {
|
||||||
return gcObjectScanner{}
|
return gcObjectScanner{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gcObjectScanner struct {
|
||||||
|
}
|
||||||
|
|
||||||
func (scanner *gcObjectScanner) pointerFree() bool {
|
func (scanner *gcObjectScanner) pointerFree() bool {
|
||||||
// We don't know whether this object contains pointers, so conservatively
|
// We don't know whether this object contains pointers, so conservatively
|
||||||
// return false.
|
// return false.
|
||||||
|
|||||||
+19
-12
@@ -59,19 +59,19 @@ import "unsafe"
|
|||||||
|
|
||||||
const preciseHeap = true
|
const preciseHeap = true
|
||||||
|
|
||||||
type gcObjectScanner struct {
|
// parseGCLayout stores the layout information passed to alloc into a gcLayout value.
|
||||||
index uintptr
|
func parseGCLayout(layout unsafe.Pointer) gcLayout {
|
||||||
size uintptr
|
return gcLayout{layout: uintptr(layout)}
|
||||||
bitmap uintptr
|
|
||||||
bitmapAddr unsafe.Pointer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGCObjectScanner(block gcBlock) gcObjectScanner {
|
// gcLayout tracks pointer locations in a heap object.
|
||||||
if gcAsserts && block != block.findHead() {
|
type gcLayout struct {
|
||||||
runtimePanic("gc: object scanner must start at head")
|
layout uintptr
|
||||||
}
|
}
|
||||||
scanner := gcObjectScanner{}
|
|
||||||
layout := *(*uintptr)(unsafe.Pointer(block.address()))
|
// scanner creates a gcObjectScanner with this layout.
|
||||||
|
func (l gcLayout) scanner() (scanner gcObjectScanner) {
|
||||||
|
layout := l.layout
|
||||||
if layout == 0 {
|
if layout == 0 {
|
||||||
// Unknown layout. Assume all words in the object could be pointers.
|
// Unknown layout. Assume all words in the object could be pointers.
|
||||||
// This layout value below corresponds to a slice of pointers like:
|
// This layout value below corresponds to a slice of pointers like:
|
||||||
@@ -104,7 +104,14 @@ func newGCObjectScanner(block gcBlock) gcObjectScanner {
|
|||||||
scanner.size = *(*uintptr)(layoutAddr)
|
scanner.size = *(*uintptr)(layoutAddr)
|
||||||
scanner.bitmapAddr = unsafe.Add(layoutAddr, unsafe.Sizeof(uintptr(0)))
|
scanner.bitmapAddr = unsafe.Add(layoutAddr, unsafe.Sizeof(uintptr(0)))
|
||||||
}
|
}
|
||||||
return scanner
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type gcObjectScanner struct {
|
||||||
|
index uintptr
|
||||||
|
size uintptr
|
||||||
|
bitmap uintptr
|
||||||
|
bitmapAddr unsafe.Pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (scanner *gcObjectScanner) pointerFree() bool {
|
func (scanner *gcObjectScanner) pointerFree() bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user