Skip to content

Commit 65d65c1

Browse files
aykevldeadprogram
authored andcommitted
wasm: fix GC scanning of allocas
Scanning of allocas was entirely broken on WebAssembly. The code intended to do this was never run. There were also no tests. Looking into this further, I found that it is actually not really necessary to do that: the C stack can be scanned conservatively and in fact this was already done for goroutine stacks (because they live on the heap and are always referenced). It wasn't done for the system stack however. With these fixes, I believe code should be both faster *and* more correct. I found this in my work to get opaque pointers supported in LLVM 15, because the code that was never reached now finally got run and was actually quite buggy.
1 parent 6b46ae2 commit 65d65c1

File tree

7 files changed

+57
-54
lines changed

7 files changed

+57
-54
lines changed

src/runtime/arch_tinygowasm.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ var (
5858

5959
globalsStart = uintptr(unsafe.Pointer(&globalsStartSymbol))
6060
globalsEnd = uintptr(unsafe.Pointer(&heapStartSymbol))
61+
62+
stackTop = uintptr(unsafe.Pointer(&globalsStartSymbol))
6163
)
6264

6365
func align(ptr uintptr) uintptr {
@@ -67,6 +69,7 @@ func align(ptr uintptr) uintptr {
6769
return (ptr + heapAlign - 1) &^ (heapAlign - 1)
6870
}
6971

72+
//export tinygo_getCurrentStackPointer
7073
func getCurrentStackPointer() uintptr
7174

7275
// growHeap tries to grow the heap size. It returns true if it succeeds, false

src/runtime/asm_tinygowasm.S

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.globaltype __stack_pointer, i32
2+
3+
.global tinygo_getCurrentStackPointer
4+
.hidden tinygo_getCurrentStackPointer
5+
.type tinygo_getCurrentStackPointer,@function
6+
tinygo_getCurrentStackPointer: // func getCurrentStackPointer() uintptr
7+
.functype tinygo_getCurrentStackPointer() -> (i32)
8+
global.get __stack_pointer
9+
return
10+
end_function

src/runtime/gc_stack_portable.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package runtime
55

66
import (
7+
"internal/task"
78
"unsafe"
89
)
910

@@ -17,19 +18,28 @@ type stackChainObject struct {
1718

1819
// markStack marks all root pointers found on the stack.
1920
//
20-
// This implementation is conservative and relies on the compiler inserting code
21-
// to manually push/pop stack objects that are stored in a linked list starting
22-
// with stackChainStart. Manually keeping track of stack values is _much_ more
23-
// expensive than letting the compiler do it and it inhibits a few important
24-
// optimizations, but it has the big advantage of being portable to basically
25-
// any ISA, including WebAssembly.
21+
// - Goroutine stacks are heap allocated and always reachable in some way
22+
// (for example through internal/task.currentTask) so they will always be
23+
// scanned.
24+
// - The system stack (aka startup stack) is not heap allocated, so even
25+
// though it may be referenced it will not be scanned by default.
26+
//
27+
// Therefore, we only need to scan the system stack.
28+
// It is relatively easy to scan the system stack while we're on it: we can
29+
// simply read __stack_pointer and __global_base and scan the area inbetween.
30+
// Unfortunately, it's hard to get the system stack pointer while we're on a
31+
// goroutine stack. But when we're on a goroutine stack, the system stack is in
32+
// the scheduler which means there shouldn't be anything on the system stack
33+
// anyway.
34+
// ...I hope this assumption holds, otherwise we will need to store the system
35+
// stack in a global or something.
36+
//
37+
// The compiler also inserts code to store all globals in a chain via
38+
// stackChainStart. Luckily we don't need to scan these, as these globals are
39+
// stored on the goroutine stack and are therefore already getting scanned.
2640
func markStack() {
27-
stackObject := stackChainStart
28-
for stackObject != nil {
29-
start := uintptr(unsafe.Pointer(stackObject)) + unsafe.Sizeof(uintptr(0))*2
30-
end := start + stackObject.numSlots*unsafe.Alignof(uintptr(0))
31-
markRoots(start, end)
32-
stackObject = stackObject.parent
41+
if task.OnSystemStack() {
42+
markRoots(getCurrentStackPointer(), stackTop)
3343
}
3444
}
3545

targets/wasi.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
"--stack-first",
1919
"--no-demangle"
2020
],
21+
"extra-files": [
22+
"src/runtime/asm_tinygowasm.S"
23+
],
2124
"emulator": "wasmtime {}",
2225
"wasm-abi": "generic"
2326
}

targets/wasm.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"--stack-first",
2020
"--no-demangle"
2121
],
22+
"extra-files": [
23+
"src/runtime/asm_tinygowasm.S"
24+
],
2225
"emulator": "node {root}/targets/wasm_exec.js {}",
2326
"wasm-abi": "js"
2427
}

transform/gc.go

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func MakeGCStackSlots(mod llvm.Module) bool {
139139
}
140140

141141
// Determine what to do with each call.
142-
var allocas, pointers []llvm.Value
142+
var pointers []llvm.Value
143143
for _, call := range calls {
144144
ptr := call.Operand(0)
145145
call.EraseFromParentAsInstruction()
@@ -189,16 +189,15 @@ func MakeGCStackSlots(mod llvm.Module) bool {
189189
// be optimized if needed.
190190
}
191191

192-
if !ptr.IsAAllocaInst().IsNil() {
193-
if typeHasPointers(ptr.Type().ElementType()) {
194-
allocas = append(allocas, ptr)
195-
}
196-
} else {
197-
pointers = append(pointers, ptr)
192+
if ptr := stripPointerCasts(ptr); !ptr.IsAAllocaInst().IsNil() {
193+
// Allocas don't need to be tracked because they are allocated
194+
// on the C stack which is scanned separately.
195+
continue
198196
}
197+
pointers = append(pointers, ptr)
199198
}
200199

201-
if len(allocas) == 0 && len(pointers) == 0 {
200+
if len(pointers) == 0 {
202201
// This function does not need to keep track of stack pointers.
203202
continue
204203
}
@@ -208,9 +207,6 @@ func MakeGCStackSlots(mod llvm.Module) bool {
208207
stackChainStartType, // Pointer to parent frame.
209208
uintptrType, // Number of elements in this frame.
210209
}
211-
for _, alloca := range allocas {
212-
fields = append(fields, alloca.Type().ElementType())
213-
}
214210
for _, ptr := range pointers {
215211
fields = append(fields, ptr.Type())
216212
}
@@ -235,16 +231,6 @@ func MakeGCStackSlots(mod llvm.Module) bool {
235231
stackObjectCast := builder.CreateBitCast(stackObject, stackChainStartType, "")
236232
builder.CreateStore(stackObjectCast, stackChainStart)
237233

238-
// Replace all independent allocas with GEPs in the stack object.
239-
for i, alloca := range allocas {
240-
gep := builder.CreateGEP(stackObject, []llvm.Value{
241-
llvm.ConstInt(ctx.Int32Type(), 0, false),
242-
llvm.ConstInt(ctx.Int32Type(), uint64(2+i), false),
243-
}, "")
244-
alloca.ReplaceAllUsesWith(gep)
245-
alloca.EraseFromParentAsInstruction()
246-
}
247-
248234
// Do a store to the stack object after each new pointer that is created.
249235
pointerStores := make(map[llvm.Value]struct{})
250236
for i, ptr := range pointers {
@@ -260,7 +246,7 @@ func MakeGCStackSlots(mod llvm.Module) bool {
260246
// Extract a pointer to the appropriate section of the stack object.
261247
gep := builder.CreateGEP(stackObject, []llvm.Value{
262248
llvm.ConstInt(ctx.Int32Type(), 0, false),
263-
llvm.ConstInt(ctx.Int32Type(), uint64(2+len(allocas)+i), false),
249+
llvm.ConstInt(ctx.Int32Type(), uint64(2+i), false),
264250
}, "")
265251

266252
// Store the pointer into the stack slot.

transform/llvm.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -75,26 +75,14 @@ func replaceGlobalIntWithArray(mod llvm.Module, name string, buf interface{}) ll
7575
return global
7676
}
7777

78-
// typeHasPointers returns whether this type is a pointer or contains pointers.
79-
// If the type is an aggregate type, it will check whether there is a pointer
80-
// inside.
81-
func typeHasPointers(t llvm.Type) bool {
82-
switch t.TypeKind() {
83-
case llvm.PointerTypeKind:
84-
return true
85-
case llvm.StructTypeKind:
86-
for _, subType := range t.StructElementTypes() {
87-
if typeHasPointers(subType) {
88-
return true
89-
}
78+
// stripPointerCasts strips instruction pointer casts (getelementptr and
79+
// bitcast) and returns the original value without the casts.
80+
func stripPointerCasts(value llvm.Value) llvm.Value {
81+
if !value.IsAInstruction().IsNil() {
82+
switch value.InstructionOpcode() {
83+
case llvm.GetElementPtr, llvm.BitCast:
84+
return stripPointerCasts(value.Operand(0))
9085
}
91-
return false
92-
case llvm.ArrayTypeKind:
93-
if typeHasPointers(t.ElementType()) {
94-
return true
95-
}
96-
return false
97-
default:
98-
return false
9986
}
87+
return value
10088
}

0 commit comments

Comments
 (0)