Skip to content

Commit 12d096a

Browse files
committed
runtime (gc_blocks.go): implement thread-local stack scanning
This change allows all thread stacks to be scanned in parallel. Each thread marks all objects reachable from its stack, using a thread-local scan list. The marking code now uses atomics to access GC metadata. If multiple threads try to mark the same object, the first one will responsible for scanning it.
1 parent 39e0333 commit 12d096a

File tree

9 files changed

+275
-126
lines changed

9 files changed

+275
-126
lines changed

compiler/atomic.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func (b *builder) createAtomicOp(name string) llvm.Value {
2020
val := b.getValue(b.fn.Params[1], getPos(b.fn))
2121
oldVal := b.CreateAtomicRMW(llvm.AtomicRMWBinOpAnd, ptr, val, llvm.AtomicOrderingSequentiallyConsistent, true)
2222
return oldVal
23-
case "OrInt32", "OrInt64", "OrUint32", "OrUint64", "OrUintptr":
23+
case "atomicOr8", "OrInt32", "OrInt64", "OrUint32", "OrUint64", "OrUintptr":
2424
ptr := b.getValue(b.fn.Params[0], getPos(b.fn))
2525
val := b.getValue(b.fn.Params[1], getPos(b.fn))
2626
oldVal := b.CreateAtomicRMW(llvm.AtomicRMWBinOpOr, ptr, val, llvm.AtomicOrderingSequentiallyConsistent, true)
@@ -37,7 +37,7 @@ func (b *builder) createAtomicOp(name string) llvm.Value {
3737
tuple := b.CreateAtomicCmpXchg(ptr, old, newVal, llvm.AtomicOrderingSequentiallyConsistent, llvm.AtomicOrderingSequentiallyConsistent, true)
3838
swapped := b.CreateExtractValue(tuple, 1, "")
3939
return swapped
40-
case "LoadInt32", "LoadInt64", "LoadUint32", "LoadUint64", "LoadUintptr", "LoadPointer":
40+
case "atomicLoad8", "LoadInt32", "LoadInt64", "LoadUint32", "LoadUint64", "LoadUintptr", "LoadPointer":
4141
ptr := b.getValue(b.fn.Params[0], getPos(b.fn))
4242
val := b.CreateLoad(b.getLLVMType(b.fn.Signature.Results().At(0).Type()), ptr, "")
4343
val.SetOrdering(llvm.AtomicOrderingSequentiallyConsistent)

compiler/intrinsics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (b *builder) defineIntrinsicFunction() {
3333
b.createVolatileLoad()
3434
case strings.HasPrefix(name, "runtime/volatile.Store"):
3535
b.createVolatileStore()
36-
case strings.HasPrefix(name, "sync/atomic.") && token.IsExported(b.fn.Name()):
36+
case (strings.HasPrefix(name, "sync/atomic.") && token.IsExported(b.fn.Name())) || strings.HasPrefix(name, "runtime.atomic"):
3737
b.createFunctionStart(true)
3838
returnValue := b.createAtomicOp(b.fn.Name())
3939
if !returnValue.IsNil() {

src/internal/task/task_threads.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,10 @@ void* tinygo_task_current(void) {
147147
void tinygo_task_send_gc_signal(pthread_t thread) {
148148
pthread_kill(thread, taskPauseSignal);
149149
}
150+
151+
// The local gc scan list.
152+
static __thread void *gcScanList;
153+
154+
void* tinygo_scan_list(void) {
155+
return &gcScanList;
156+
}

src/internal/task/task_threads.go

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,6 @@ func taskExited(t *Task) {
147147
}
148148
}
149149

150-
// scanWaitGroup is used to wait on until all threads have finished the current state transition.
151-
var scanWaitGroup waitGroup
152-
153150
type waitGroup struct {
154151
f Futex
155152
}
@@ -176,109 +173,9 @@ func (wg *waitGroup) wait() {
176173
}
177174
}
178175

179-
// gcState is used to track and notify threads when the GC is stopping/resuming.
180-
var gcState Futex
181-
182-
const (
183-
gcStateResumed = iota
184-
gcStateStopped
185-
)
186-
187-
// GC scan phase. Because we need to stop the world while scanning, this kinda
188-
// needs to be done in the tasks package.
189-
//
190-
// After calling this function, GCResumeWorld needs to be called once to resume
191-
// all threads again.
192-
func GCStopWorldAndScan() {
193-
current := Current()
194-
195-
// NOTE: This does not need to be atomic.
196-
if gcState.Load() == gcStateResumed {
197-
// Don't allow new goroutines to be started while pausing/resuming threads
198-
// in the stop-the-world phase.
199-
activeTaskLock.Lock()
200-
201-
// Wait for threads to finish resuming.
202-
scanWaitGroup.wait()
203-
204-
// Change the gc state to stopped.
205-
// NOTE: This does not need to be atomic.
206-
gcState.Store(gcStateStopped)
207-
208-
// Set the number of threads to wait for.
209-
scanWaitGroup = initWaitGroup(otherGoroutines)
210-
211-
// Pause all other threads.
212-
for t := activeTasks; t != nil; t = t.state.QueueNext {
213-
if t != current {
214-
tinygo_task_send_gc_signal(t.state.thread)
215-
}
216-
}
217-
218-
// Wait for the threads to finish stopping.
219-
scanWaitGroup.wait()
220-
}
221-
222-
// Scan other thread stacks.
223-
for t := activeTasks; t != nil; t = t.state.QueueNext {
224-
if t != current {
225-
markRoots(t.state.stackBottom, t.state.stackTop)
226-
}
227-
}
228-
229-
// Scan the current stack, and all current registers.
230-
scanCurrentStack()
231-
232-
// Scan all globals (implemented in the runtime).
233-
gcScanGlobals()
234-
}
235-
236-
// After the GC is done scanning, resume all other threads.
237-
func GCResumeWorld() {
238-
// NOTE: This does not need to be atomic.
239-
if gcState.Load() == gcStateResumed {
240-
// This is already resumed.
241-
return
242-
}
243-
244-
// Set the wait group to track resume progress.
245-
scanWaitGroup = initWaitGroup(otherGoroutines)
246-
247-
// Set the state to resumed.
248-
gcState.Store(gcStateResumed)
249-
250-
// Wake all of the stopped threads.
251-
gcState.WakeAll()
252-
253-
// Allow goroutines to start and exit again.
254-
activeTaskLock.Unlock()
255-
}
256-
257-
//go:linkname markRoots runtime.markRoots
258-
func markRoots(start, end uintptr)
259-
260176
// Scan globals, implemented in the runtime package.
261177
func gcScanGlobals()
262178

263-
var stackScanLock PMutex
264-
265-
//export tinygo_task_gc_pause
266-
func tingyo_task_gc_pause(sig int32) {
267-
// Write the entrty stack pointer to the state.
268-
Current().state.stackBottom = uintptr(stacksave())
269-
270-
// Notify the GC that we are stopped.
271-
scanWaitGroup.done()
272-
273-
// Wait for the GC to resume.
274-
for gcState.Load() == gcStateStopped {
275-
gcState.Wait(gcStateStopped)
276-
}
277-
278-
// Notify the GC that we have resumed.
279-
scanWaitGroup.done()
280-
}
281-
282179
//go:export tinygo_scanCurrentStack
283180
func scanCurrentStack()
284181

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//go:build scheduler.threads && (gc.conservative || gc.precise)
2+
3+
package task
4+
5+
// stopWaitGroup is used to wait until all threads have stopped.
6+
var stopWaitGroup waitGroup
7+
8+
// scanWaitGroup is used to wait until all threads have finished scanning their stacks.
9+
var scanWaitGroup waitGroup
10+
11+
// resumeWaitGroup is used to wait until all threads have resumed.
12+
var resumeWaitGroup waitGroup
13+
14+
// GC scan phase. Because we need to stop the world while scanning, this kinda
15+
// needs to be done in the tasks package.
16+
//
17+
// After calling this function, GCResumeWorld needs to be called once to resume
18+
// all threads again.
19+
func GCStopWorldAndScan() {
20+
// Wait for threads to resume from the previous scan.
21+
resumeWaitGroup.wait()
22+
23+
// Don't allow new goroutines to be started while pausing/resuming threads
24+
// in the stop-the-world phase.
25+
activeTaskLock.Lock()
26+
27+
// Set the number of threads to wait for.
28+
otherGoroutines := otherGoroutines
29+
stopWaitGroup = initWaitGroup(otherGoroutines)
30+
scanWaitGroup = initWaitGroup(otherGoroutines + 1)
31+
resumeWaitGroup = initWaitGroup(otherGoroutines)
32+
33+
// Pause all other threads.
34+
current := Current()
35+
for t := activeTasks; t != nil; t = t.state.QueueNext {
36+
if t != current {
37+
tinygo_task_send_gc_signal(t.state.thread)
38+
}
39+
}
40+
41+
// Wait for everything to stop.
42+
stopWaitGroup.wait()
43+
44+
// Scan all globals (implemented in the runtime).
45+
gcScanGlobals()
46+
47+
// Scan our stack and wait for everything else to complete.
48+
localScan()
49+
}
50+
51+
//export tinygo_task_gc_pause
52+
func tingyo_task_gc_pause(sig int32) {
53+
// We have stopped.
54+
stopWaitGroup.done()
55+
56+
// Wait for all other threads to stop.
57+
stopWaitGroup.wait()
58+
59+
// Scan the local stack.
60+
localScan()
61+
62+
// We are resuming.
63+
resumeWaitGroup.done()
64+
}
65+
66+
func localScan() {
67+
// Scan the current stack, and all current registers.
68+
scanCurrentStack()
69+
70+
// Assist scanning of heap objects.
71+
finishMark()
72+
73+
// We are done scanning.
74+
scanWaitGroup.done()
75+
76+
// Wait for all other threads to finish scanning.
77+
scanWaitGroup.wait()
78+
}
79+
80+
//go:linkname finishMark runtime.finishMark
81+
func finishMark()
82+
83+
// GCResumeWorld does not resume anything with the blocks collector.
84+
// The threads will resume as soon as the scan completes.
85+
func GCResumeWorld() {
86+
// Allow goroutines to start and exit again.
87+
activeTaskLock.Unlock()
88+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//go:build scheduler.threads && !(gc.conservative || gc.precise)
2+
3+
package task
4+
5+
// scanWaitGroup is used to wait on until all threads have finished the current state transition.
6+
var scanWaitGroup waitGroup
7+
8+
// gcState is used to track and notify threads when the GC is stopping/resuming.
9+
var gcState Futex
10+
11+
const (
12+
gcStateResumed = iota
13+
gcStateStopped
14+
)
15+
16+
// GC scan phase. Because we need to stop the world while scanning, this kinda
17+
// needs to be done in the tasks package.
18+
//
19+
// After calling this function, GCResumeWorld needs to be called once to resume
20+
// all threads again.
21+
func GCStopWorldAndScan() {
22+
current := Current()
23+
24+
// NOTE: This does not need to be atomic.
25+
if gcState.Load() == gcStateResumed {
26+
// Don't allow new goroutines to be started while pausing/resuming threads
27+
// in the stop-the-world phase.
28+
activeTaskLock.Lock()
29+
30+
// Wait for threads to finish resuming.
31+
scanWaitGroup.wait()
32+
33+
// Change the gc state to stopped.
34+
// NOTE: This does not need to be atomic.
35+
gcState.Store(gcStateStopped)
36+
37+
// Set the number of threads to wait for.
38+
scanWaitGroup = initWaitGroup(otherGoroutines)
39+
40+
// Pause all other threads.
41+
for t := activeTasks; t != nil; t = t.state.QueueNext {
42+
if t != current {
43+
tinygo_task_send_gc_signal(t.state.thread)
44+
}
45+
}
46+
47+
// Wait for the threads to finish stopping.
48+
scanWaitGroup.wait()
49+
}
50+
51+
// Scan other thread stacks.
52+
for t := activeTasks; t != nil; t = t.state.QueueNext {
53+
if t != current {
54+
markRoots(t.state.stackBottom, t.state.stackTop)
55+
}
56+
}
57+
58+
// Scan the current stack, and all current registers.
59+
scanCurrentStack()
60+
61+
// Scan all globals (implemented in the runtime).
62+
gcScanGlobals()
63+
}
64+
65+
// After the GC is done scanning, resume all other threads.
66+
func GCResumeWorld() {
67+
// NOTE: This does not need to be atomic.
68+
if gcState.Load() == gcStateResumed {
69+
// This is already resumed.
70+
return
71+
}
72+
73+
// Set the wait group to track resume progress.
74+
scanWaitGroup = initWaitGroup(otherGoroutines)
75+
76+
// Set the state to resumed.
77+
gcState.Store(gcStateResumed)
78+
79+
// Wake all of the stopped threads.
80+
gcState.WakeAll()
81+
82+
// Allow goroutines to start and exit again.
83+
activeTaskLock.Unlock()
84+
}
85+
86+
//go:linkname markRoots runtime.markRoots
87+
func markRoots(start, end uintptr)
88+
89+
//export tinygo_task_gc_pause
90+
func tingyo_task_gc_pause(sig int32) {
91+
// Write the entrty stack pointer to the state.
92+
Current().state.stackBottom = uintptr(stacksave())
93+
94+
// Notify the GC that we are stopped.
95+
scanWaitGroup.done()
96+
97+
// Wait for the GC to resume.
98+
for gcState.Load() == gcStateStopped {
99+
gcState.Wait(gcStateStopped)
100+
}
101+
102+
// Notify the GC that we have resumed.
103+
scanWaitGroup.done()
104+
}

0 commit comments

Comments
 (0)