Skip to content

Commit cfbf6c9

Browse files
committed
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
1 parent e819901 commit cfbf6c9

File tree

4 files changed

+105
-103
lines changed

4 files changed

+105
-103
lines changed

builder/sizes_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) {
4242
// This is a small number of very diverse targets that we want to test.
4343
tests := []sizeTest{
4444
// microcontrollers
45-
{"hifive1b", "examples/echo", 3896, 280, 0, 2268},
46-
{"microbit", "examples/serial", 2860, 360, 8, 2272},
47-
{"wioterminal", "examples/pininterrupt", 7361, 1491, 116, 6912},
45+
{"hifive1b", "examples/echo", 3756, 280, 0, 2268},
46+
{"microbit", "examples/serial", 2756, 340, 8, 2272},
47+
{"wioterminal", "examples/pininterrupt", 7297, 1491, 116, 6912},
4848

4949
// TODO: also check wasm. Right now this is difficult, because
5050
// wasm binaries are run through wasm-opt and therefore the

src/runtime/gc_blocks.go

Lines changed: 68 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ const (
4646
bytesPerBlock = wordsPerBlock * unsafe.Sizeof(heapStart)
4747
stateBits = 2 // how many bits a block state takes (see blockState type)
4848
blocksPerStateByte = 8 / stateBits
49-
markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan
5049
)
5150

5251
var (
5352
metadataStart unsafe.Pointer // pointer to the start of the heap metadata
53+
scanList *objHeader // scanList is a singly linked list of heap objects that have been marked but not scanned
5454
nextAlloc gcBlock // the next block that should be tried by the allocator
5555
endBlock gcBlock // the block just past the end of the available space
5656
gcTotalAlloc uint64 // total number of bytes allocated
@@ -225,6 +225,15 @@ func (b gcBlock) unmark() {
225225
}
226226
}
227227

228+
// objHeader is a structure prepended to every heap object to hold metadata.
229+
type objHeader struct {
230+
// next is the next object to scan after this.
231+
next *objHeader
232+
233+
// layout holds the layout bitmap used to find pointers in the object.
234+
layout gcLayout
235+
}
236+
228237
func isOnHeap(ptr uintptr) bool {
229238
return ptr >= heapStart && ptr < uintptr(metadataStart)
230239
}
@@ -315,13 +324,10 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
315324
runtimePanicAt(returnAddress(0), "heap alloc in interrupt")
316325
}
317326

318-
// Round the size up to a multiple of blocks.
327+
// Round the size up to a multiple of blocks, adding space for the header.
319328
rawSize := size
329+
size += align(unsafe.Sizeof(objHeader{}))
320330
size += bytesPerBlock - 1
321-
if preciseHeap {
322-
// Add space for the layout.
323-
size += align(unsafe.Sizeof(layout))
324-
}
325331
if size < rawSize {
326332
// The size overflowed.
327333
runtimePanicAt(returnAddress(0), "out of memory")
@@ -414,20 +420,18 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
414420
i.setState(blockStateTail)
415421
}
416422

423+
// Create the object header.
424+
pointer := thisAlloc.pointer()
425+
header := (*objHeader)(pointer)
426+
header.layout = parseGCLayout(layout)
427+
417428
// We've claimed this allocation, now we can unlock the heap.
418429
gcLock.Unlock()
419430

420431
// Return a pointer to this allocation.
421-
pointer := thisAlloc.pointer()
422-
if preciseHeap {
423-
// Store the object layout at the start of the object.
424-
// TODO: this wastes a little bit of space on systems with
425-
// larger-than-pointer alignment requirements.
426-
*(*unsafe.Pointer)(pointer) = layout
427-
add := align(unsafe.Sizeof(layout))
428-
pointer = unsafe.Add(pointer, add)
429-
size -= add
430-
}
432+
add := align(unsafe.Sizeof(objHeader{}))
433+
pointer = unsafe.Add(pointer, add)
434+
size -= add
431435
memzero(pointer, size)
432436
return pointer
433437
}
@@ -562,42 +566,33 @@ func markCurrentGoroutineStack(sp uintptr) {
562566
markRoot(0, sp)
563567
}
564568

565-
// stackOverflow is a flag which is set when the GC scans too deep while marking.
566-
// After it is set, all marked allocations must be re-scanned.
567-
var stackOverflow bool
568-
569-
// startMark starts the marking process on a root and all of its children.
570-
func startMark(root gcBlock) {
571-
var stack [markStackSize]gcBlock
572-
stack[0] = root
573-
root.setState(blockStateMark)
574-
stackLen := 1
575-
for stackLen > 0 {
576-
// Pop a block off of the stack.
577-
stackLen--
578-
block := stack[stackLen]
579-
if gcDebug {
580-
println("stack popped, remaining stack:", stackLen)
569+
// finishMark finishes the marking process by scanning all heap objects on scanList.
570+
func finishMark() {
571+
for {
572+
// Remove an object from the scan list.
573+
obj := scanList
574+
if obj == nil {
575+
return
581576
}
577+
scanList = obj.next
582578

583-
// Scan all pointers inside the block.
584-
scanner := newGCObjectScanner(block)
579+
// Create a scanner with the object layout.
580+
scanner := obj.layout.scanner()
585581
if scanner.pointerFree() {
586582
// This object doesn't contain any pointers.
587583
// This is a fast path for objects like make([]int, 4096).
588584
continue
589585
}
590-
start, end := block.address(), block.findNext().address()
591-
if preciseHeap {
592-
// The first word of the object is just the pointer layout value.
593-
// Skip it.
594-
start += align(unsafe.Sizeof(uintptr(0)))
595-
}
586+
587+
// Scan all pointers in the object.
588+
start := uintptr(unsafe.Pointer(obj)) + align(unsafe.Sizeof(objHeader{}))
589+
end := blockFromAddr(uintptr(unsafe.Pointer(obj))).findNext().address()
590+
596591
for addr := start; addr != end; addr += unsafe.Alignof(addr) {
597592
// Load the word.
598593
word := *(*uintptr)(unsafe.Pointer(addr))
599594

600-
if !scanner.nextIsPointer(word, root.address(), addr) {
595+
if !scanner.nextIsPointer(word, uintptr(unsafe.Pointer(obj)), addr) {
601596
// Not a heap pointer.
602597
continue
603598
}
@@ -628,58 +623,46 @@ func startMark(root gcBlock) {
628623
}
629624
referencedBlock.setState(blockStateMark)
630625

631-
if stackLen == len(stack) {
632-
// The stack is full.
633-
// It is necessary to rescan all marked blocks once we are done.
634-
stackOverflow = true
635-
if gcDebug {
636-
println("gc stack overflowed")
637-
}
638-
continue
639-
}
640-
641-
// Push the pointer onto the stack to be scanned later.
642-
stack[stackLen] = referencedBlock
643-
stackLen++
626+
// Add the object to the scan list.
627+
header := (*objHeader)(referencedBlock.pointer())
628+
header.next = scanList
629+
scanList = header
644630
}
645631
}
646632
}
647633

648-
// finishMark finishes the marking process by processing all stack overflows.
649-
func finishMark() {
650-
for stackOverflow {
651-
// Re-mark all blocks.
652-
stackOverflow = false
653-
for block := gcBlock(0); block < endBlock; block++ {
654-
if block.state() != blockStateMark {
655-
// Block is not marked, so we do not need to rescan it.
656-
continue
657-
}
634+
// mark a GC root at the address addr.
635+
func markRoot(addr, root uintptr) {
636+
// Find the heap block corresponding to the root.
637+
if !isOnHeap(root) {
638+
// This is not a heap pointer.
639+
return
640+
}
641+
block := blockFromAddr(root)
658642

659-
// Re-mark the block.
660-
startMark(block)
661-
}
643+
// Find the head of the corresponding object.
644+
if block.state() == blockStateFree {
645+
// The to-be-marked object doesn't actually exist.
646+
// This could either be a dangling pointer (oops!) but most likely
647+
// just a false positive.
648+
return
662649
}
663-
}
650+
head := block.findHead()
664651

665-
// mark a GC root at the address addr.
666-
func markRoot(addr, root uintptr) {
667-
if isOnHeap(root) {
668-
block := blockFromAddr(root)
669-
if block.state() == blockStateFree {
670-
// The to-be-marked object doesn't actually exist.
671-
// This could either be a dangling pointer (oops!) but most likely
672-
// just a false positive.
673-
return
674-
}
675-
head := block.findHead()
676-
if head.state() != blockStateMark {
677-
if gcDebug {
678-
println("found unmarked pointer", root, "at address", addr)
679-
}
680-
startMark(head)
681-
}
652+
// Mark the object.
653+
if head.state() == blockStateMark {
654+
// This object is already marked.
655+
return
656+
}
657+
if gcDebug {
658+
println("found unmarked pointer", root, "at address", addr)
682659
}
660+
head.setState(blockStateMark)
661+
662+
// Add the object to the scan list.
663+
header := (*objHeader)(head.pointer())
664+
header.next = scanList
665+
scanList = header
683666
}
684667

685668
// Sweep goes through all memory and frees unmarked memory.

src/runtime/gc_conservative.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,27 @@
66

77
package runtime
88

9-
const preciseHeap = false
9+
import "unsafe"
1010

11-
type gcObjectScanner struct {
11+
// gcLayout tracks pointer locations in a heap object.
12+
// The conservative GC treats all locations as potential pointers, so this doesn't need to store anything.
13+
type gcLayout struct {
14+
}
15+
16+
// parseGCLayout stores the layout information passed to alloc into a gcLayout value.
17+
// The conservative GC discards this information.
18+
func parseGCLayout(layout unsafe.Pointer) gcLayout {
19+
return gcLayout{}
1220
}
1321

14-
func newGCObjectScanner(block gcBlock) gcObjectScanner {
22+
// scanner creates a gcObjectScanner with this layout.
23+
func (l gcLayout) scanner() gcObjectScanner {
1524
return gcObjectScanner{}
1625
}
1726

27+
type gcObjectScanner struct {
28+
}
29+
1830
func (scanner *gcObjectScanner) pointerFree() bool {
1931
// We don't know whether this object contains pointers, so conservatively
2032
// return false.

src/runtime/gc_precise.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,19 @@ import "unsafe"
5959

6060
const preciseHeap = true
6161

62-
type gcObjectScanner struct {
63-
index uintptr
64-
size uintptr
65-
bitmap uintptr
66-
bitmapAddr unsafe.Pointer
62+
// parseGCLayout stores the layout information passed to alloc into a gcLayout value.
63+
func parseGCLayout(layout unsafe.Pointer) gcLayout {
64+
return gcLayout{layout: uintptr(layout)}
6765
}
6866

69-
func newGCObjectScanner(block gcBlock) gcObjectScanner {
70-
if gcAsserts && block != block.findHead() {
71-
runtimePanic("gc: object scanner must start at head")
72-
}
73-
scanner := gcObjectScanner{}
74-
layout := *(*uintptr)(unsafe.Pointer(block.address()))
67+
// gcLayout tracks pointer locations in a heap object.
68+
type gcLayout struct {
69+
layout uintptr
70+
}
71+
72+
// scanner creates a gcObjectScanner with this layout.
73+
func (l gcLayout) scanner() (scanner gcObjectScanner) {
74+
layout := l.layout
7575
if layout == 0 {
7676
// Unknown layout. Assume all words in the object could be pointers.
7777
// This layout value below corresponds to a slice of pointers like:
@@ -104,7 +104,14 @@ func newGCObjectScanner(block gcBlock) gcObjectScanner {
104104
scanner.size = *(*uintptr)(layoutAddr)
105105
scanner.bitmapAddr = unsafe.Add(layoutAddr, unsafe.Sizeof(uintptr(0)))
106106
}
107-
return scanner
107+
return
108+
}
109+
110+
type gcObjectScanner struct {
111+
index uintptr
112+
size uintptr
113+
bitmap uintptr
114+
bitmapAddr unsafe.Pointer
108115
}
109116

110117
func (scanner *gcObjectScanner) pointerFree() bool {

0 commit comments

Comments
 (0)