@@ -4,6 +4,11 @@ import (
44 "tinygo.org/x/go-llvm"
55)
66
7+ // This is somewhat ugly to access through the API.
8+ // https://github.com/llvm/llvm-project/blob/94ebcfd16dac67486bae624f74e1c5c789448bae/llvm/include/llvm/Support/ModRef.h#L62
9+ // https://github.com/llvm/llvm-project/blob/94ebcfd16dac67486bae624f74e1c5c789448bae/llvm/include/llvm/Support/ModRef.h#L87
10+ const shiftExcludeArgMem = 2
11+
712// MakeGCStackSlots converts all calls to runtime.trackPointer to explicit
813// stores to stack slots that are scannable by the GC.
914func MakeGCStackSlots (mod llvm.Module ) bool {
@@ -36,54 +41,63 @@ func MakeGCStackSlots(mod llvm.Module) bool {
3641 defer targetData .Dispose ()
3742 uintptrType := ctx .IntType (targetData .PointerSize () * 8 )
3843
39- // Look at *all* functions to see whether they are free of function pointer
44+ // All functions that call runtime.alloc needs stack objects.
45+ trackFuncs := map [llvm.Value ]struct {}{}
46+ markParentFunctions (trackFuncs , alloc )
47+
48+ // External functions may indirectly suspend the goroutine or perform a heap allocation.
49+ // Their callers should get stack objects.
50+ memAttr := llvm .AttributeKindID ("memory" )
51+ for fn := mod .FirstFunction (); ! fn .IsNil (); fn = llvm .NextFunction (fn ) {
52+ if _ , ok := trackFuncs [fn ]; ok {
53+ continue // already found
54+ }
55+ if ! fn .FirstBasicBlock ().IsNil () {
56+ // This is not an external function.
57+ continue
58+ }
59+ if fn == trackPointer {
60+ // Manually exclude trackPointer.
61+ continue
62+ }
63+
64+ mem := fn .GetEnumFunctionAttribute (memAttr )
65+ if ! mem .IsNil () && mem .GetEnumValue ()>> shiftExcludeArgMem == 0 {
66+ // This does not access non-argument memory.
67+ // Exclude it.
68+ continue
69+ }
70+
71+ // The callers need stack objects.
72+ markParentFunctions (trackFuncs , fn )
73+ }
74+
75+ // Look at all other functions to see whether they contain function pointer
4076 // calls.
4177 // This takes less than 5ms for ~100kB of WebAssembly but would perhaps be
4278 // faster when written in C++ (to avoid the CGo overhead).
43- funcsWithFPCall := map [llvm.Value ]struct {}{}
44- n := 0
4579 for fn := mod .FirstFunction (); ! fn .IsNil (); fn = llvm .NextFunction (fn ) {
46- n ++
47- if _ , ok := funcsWithFPCall [fn ]; ok {
80+ if _ , ok := trackFuncs [fn ]; ok {
4881 continue // already found
4982 }
50- done := false
51- for bb := fn .FirstBasicBlock (); ! bb .IsNil () && ! done ; bb = llvm .NextBasicBlock (bb ) {
52- for call := bb .FirstInstruction (); ! call .IsNil () && ! done ; call = llvm .NextInstruction (call ) {
83+
84+ scanBody:
85+ for bb := fn .FirstBasicBlock (); ! bb .IsNil (); bb = llvm .NextBasicBlock (bb ) {
86+ for call := bb .FirstInstruction (); ! call .IsNil (); call = llvm .NextInstruction (call ) {
5387 if call .IsACallInst ().IsNil () {
5488 continue // only looking at calls
5589 }
5690 called := call .CalledValue ()
5791 if ! called .IsAFunction ().IsNil () {
5892 continue // only looking for function pointers
5993 }
60- funcsWithFPCall [fn ] = struct {}{}
61- markParentFunctions (funcsWithFPCall , fn )
62- done = true
94+ trackFuncs [fn ] = struct {}{}
95+ markParentFunctions (trackFuncs , fn )
96+ break scanBody
6397 }
6498 }
6599 }
66100
67- // Determine which functions need stack objects. Many leaf functions don't
68- // need it: it only causes overhead for them.
69- // Actually, in one test it was only able to eliminate stack object from 12%
70- // of functions that had a call to runtime.trackPointer (8 out of 68
71- // functions), so this optimization is not as big as it may seem.
72- allocatingFunctions := map [llvm.Value ]struct {}{} // set of allocating functions
73-
74- // Work from runtime.alloc and trace all parents to check which functions do
75- // a heap allocation (and thus which functions do not).
76- markParentFunctions (allocatingFunctions , alloc )
77-
78- // Also trace all functions that call a function pointer.
79- for fn := range funcsWithFPCall {
80- // Assume that functions that call a function pointer do a heap
81- // allocation as a conservative guess because the called function might
82- // do a heap allocation.
83- allocatingFunctions [fn ] = struct {}{}
84- markParentFunctions (allocatingFunctions , fn )
85- }
86-
87101 // Collect some variables used below in the loop.
88102 stackChainStart := mod .NamedGlobal ("runtime.stackChainStart" )
89103 if stackChainStart .IsNil () {
@@ -110,7 +124,7 @@ func MakeGCStackSlots(mod llvm.Module) bool {
110124 // Pick the parent function.
111125 fn := call .InstructionParent ().Parent ()
112126
113- if _ , ok := allocatingFunctions [fn ]; ! ok {
127+ if _ , ok := trackFuncs [fn ]; ! ok {
114128 // This function nor any of the functions it calls (recursively)
115129 // allocate anything from the heap, so it will not trigger a garbage
116130 // collection cycle. Thus, it does not need to track local pointer
0 commit comments