Skip to content

Commit

Permalink
runtime: use the main (startup) stack for the main goroutine
Browse files Browse the repository at this point in the history
Instead of always starting a new goroutine for the main goroutine, run
the main goroutine on the system stack.
The system stack is not occupied with scheduling, instead each goroutine
that wants to pause itself calls into the scheduler which will switch to
the next task (goroutine) to run, or sleeps.

There are various advantages of this over the previous system:

  * When the program doesn't start a goroutine, the code size and RAM
    consumption is close to what you'd get with `-scheduler=none`.
  * When the program does start a goroutine, there is still a reduction
    in RAM consumption because only one extra stack is needed.
  * Because tasks directly switch to the next task to run, only a single
    task switch is needed instead of two (goroutine -> scheduler ->
    goroutine). This should improve task switching performance.

I kept the current behavior for WebAssembly/Asyncify. I looked into how
the same benefits can be realized for WebAssembly but couldn't easily
find how to do that. Maybe this can be done separately, or maybe we'll
just wait for the stack switching proposal to finish.

The code for Cortex-M is currently more complicated than I'd like, and
therefore can sometimes result in a slight increase in code size. I'd
like to fix this eventually but am still looking into good ways to do
this. I still think this change is generally beneficial because many
programs see big reductions in code size when compiling for Cortex-M.
  • Loading branch information
aykevl committed Jun 18, 2022
1 parent c119721 commit 50bfb0a
Show file tree
Hide file tree
Showing 32 changed files with 209 additions and 177 deletions.
16 changes: 13 additions & 3 deletions builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -1228,10 +1228,15 @@ func determineStackSizes(mod llvm.Module, executable string) ([]string, map[stri
// Goroutines need to be started and finished and take up some stack space
// that way. This can be measured by measuing the stack size of
// tinygo_startTask.
if numFuncs := len(functions["tinygo_startTask"]); numFuncs != 1 {
return nil, nil, fmt.Errorf("expected exactly one definition of tinygo_startTask, got %d", numFuncs)
var baseStackSize uint64
var baseStackSizeType stacksize.SizeType
var baseStackSizeFailedAt *stacksize.CallNode
if len(gowrappers) != 0 {
if numFuncs := len(functions["tinygo_startTask"]); numFuncs != 1 {
return nil, nil, fmt.Errorf("expected exactly one definition of tinygo_startTask, got %d", numFuncs)
}
baseStackSize, baseStackSizeType, baseStackSizeFailedAt = functions["tinygo_startTask"][0].StackSize()
}
baseStackSize, baseStackSizeType, baseStackSizeFailedAt := functions["tinygo_startTask"][0].StackSize()

sizes := make(map[string]functionStackSize)

Expand Down Expand Up @@ -1303,6 +1308,11 @@ func modifyStackSizes(executable string, stackSizeLoads []string, stackSizes map
if err != nil {
return err
}
if data == nil {
// The .tinygo_stacksizes section doesn't exist, so assume this
// modification isn't needed.
return nil
}

if len(stackSizeLoads)*4 != len(data) {
// Note: while AVR should use 2 byte stack sizes, even 64-bit platforms
Expand Down
2 changes: 1 addition & 1 deletion builder/elfpatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func getElfSectionData(executable string, sectionName string) ([]byte, elf.FileH

section := elfFile.Section(sectionName)
if section == nil {
return nil, elf.FileHeader{}, fmt.Errorf("could not find %s section", sectionName)
return nil, elf.FileHeader{}, nil
}

data, err := section.Data()
Expand Down
10 changes: 2 additions & 8 deletions src/internal/task/task_asyncify.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ func Pause() {
//export tinygo_unwind
func (*stackState) unwind()

// Resume the task until it pauses or completes.
// Switch to this task until it pauses or completes.
// This may only be called from the scheduler.
func (t *Task) Resume() {
func (t *Task) Switch() {
// The current task must be saved and restored because this can nest on WASM with JS.
prevTask := currentTask
t.gcData.swap()
Expand All @@ -121,9 +121,3 @@ func (t *Task) Resume() {

//export tinygo_rewind
func (*state) rewind()

// OnSystemStack returns whether the caller is running on the system stack.
func OnSystemStack() bool {
// If there is not an active goroutine, then this must be running on the system stack.
return Current() == nil
}
6 changes: 3 additions & 3 deletions src/internal/task/task_none.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {

type state struct{}

func (t *Task) Resume() {
func (t *Task) Switch() {
runtimePanic("scheduler is disabled")
}

// OnSystemStack returns whether the caller is running on the system stack.
func OnSystemStack() bool {
// MainTask returns whether the caller is running in the main goroutine.
func MainTask() bool {
// This scheduler does not do any stack switching.
return true
}
35 changes: 23 additions & 12 deletions src/internal/task/task_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,49 @@ type state struct {
canaryPtr *uintptr
}

// currentTask is the current running task, or nil if currently in the scheduler.
var currentTask *Task
// The task struct for the main goroutine.
var mainTask Task

// currentTask is the current running task. The default value is the main
// goroutine.
var currentTask = &mainTask

// Current returns the current active task.
func Current() *Task {
return currentTask
}

// Pause suspends the current task and returns to the scheduler.
// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt.
// This function may only be called when running on a goroutine stack, not when in an interrupt.
func Pause() {
// Check whether the canary (the lowest address of the stack) is still
// valid. If it is not, a stack overflow has occured.
if *currentTask.state.canaryPtr != stackCanary {
if currentTask.state.canaryPtr != nil && *currentTask.state.canaryPtr != stackCanary {
runtimePanic("goroutine stack overflow")
}
currentTask.state.pause()
scheduler()
}

//go:linkname scheduler runtime.scheduler
func scheduler()

//export tinygo_pause
func pause() {
Pause()
}

// Resume the task until it pauses or completes.
// Switch to the given task until it pauses or completes.
// This may only be called from the scheduler.
func (t *Task) Resume() {
func (t *Task) Switch() {
current := currentTask
if current == t {
// Nothing to switch to: we're already in this task.
return
}
currentTask = t
t.gcData.swap()
t.state.resume()
t.state.switchTo(current)
t.gcData.swap()
currentTask = nil
}

// initialize the state and prepare to call the specified function with the specified argument bundle.
Expand Down Expand Up @@ -103,8 +114,8 @@ func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
runqueuePushBack(t)
}

// OnSystemStack returns whether the caller is running on the system stack.
func OnSystemStack() bool {
// MainTask returns whether the caller is running in the main goroutine.
func MainTask() bool {
// If there is not an active goroutine, then this must be running on the system stack.
return Current() == nil
return currentTask == &mainTask
}
7 changes: 2 additions & 5 deletions src/internal/task/task_stack_386.S
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ tinygo_startTask:
// Branch to the "goroutine start" function.
calll *%ebx

// Rebalance the stack (to undo the above push).
addl $4, %esp

// After return, exit this goroutine. This is a tail call.
jmp tinygo_pause
// After return, exit this goroutine.
calll tinygo_pause
.cfi_endproc

.global tinygo_swapTask
Expand Down
14 changes: 3 additions & 11 deletions src/internal/task/task_stack_386.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ package task

import "unsafe"

var systemStack uintptr

// calleeSavedRegs is the list of registers that must be saved and restored when
// switching between tasks. Also see task_stack_386.S that relies on the exact
// layout of this struct.
Expand Down Expand Up @@ -45,18 +43,12 @@ func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
r.esi = uintptr(args)
}

func (s *state) resume() {
swapTask(s.sp, &systemStack)
}

func (s *state) pause() {
newStack := systemStack
systemStack = 0
swapTask(newStack, &s.sp)
func (s *state) switchTo(current *Task) {
swapTask(s.sp, &current.state.sp)
}

// SystemStack returns the system stack pointer when called from a task stack.
// When called from the system stack, it returns 0.
func SystemStack() uintptr {
return systemStack
return mainTask.state.sp
}
6 changes: 3 additions & 3 deletions src/internal/task/task_stack_amd64.S
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ tinygo_startTask:
// Branch to the "goroutine start" function.
callq *%r12

// After return, exit this goroutine. This is a tail call.
// After return, exit this goroutine.
#ifdef __MACH__
jmp _tinygo_pause
callq _tinygo_pause
#else
jmp tinygo_pause
callq tinygo_pause
#endif
.cfi_endproc

Expand Down
14 changes: 3 additions & 11 deletions src/internal/task/task_stack_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ package task

import "unsafe"

var systemStack uintptr

// calleeSavedRegs is the list of registers that must be saved and restored when
// switching between tasks. Also see task_stack_amd64.S that relies on the exact
// layout of this struct.
Expand Down Expand Up @@ -44,18 +42,12 @@ func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
r.r13 = uintptr(args)
}

func (s *state) resume() {
swapTask(s.sp, &systemStack)
}

func (s *state) pause() {
newStack := systemStack
systemStack = 0
swapTask(newStack, &s.sp)
func (s *state) switchTo(current *Task) {
swapTask(s.sp, &current.state.sp)
}

// SystemStack returns the system stack pointer when called from a task stack.
// When called from the system stack, it returns 0.
func SystemStack() uintptr {
return systemStack
return mainTask.state.sp
}
4 changes: 2 additions & 2 deletions src/internal/task/task_stack_amd64_windows.S
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ tinygo_startTask:
// Branch to the "goroutine start" function.
callq *%r12

// After return, exit this goroutine. This is a tail call.
jmp tinygo_pause
// After return, exit this goroutine.
callq tinygo_pause

.global tinygo_swapTask
.section .text.tinygo_swapTask,"ax"
Expand Down
14 changes: 3 additions & 11 deletions src/internal/task/task_stack_amd64_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ package task

import "unsafe"

var systemStack uintptr

// calleeSavedRegs is the list of registers that must be saved and restored when
// switching between tasks. Also see task_stack_amd64_windows.S that relies on
// the exact layout of this struct.
Expand Down Expand Up @@ -49,18 +47,12 @@ func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
r.r13 = uintptr(args)
}

func (s *state) resume() {
swapTask(s.sp, &systemStack)
}

func (s *state) pause() {
newStack := systemStack
systemStack = 0
swapTask(newStack, &s.sp)
func (s *state) switchTo(current *Task) {
swapTask(s.sp, &current.state.sp)
}

// SystemStack returns the system stack pointer when called from a task stack.
// When called from the system stack, it returns 0.
func SystemStack() uintptr {
return systemStack
return mainTask.state.sp
}
14 changes: 3 additions & 11 deletions src/internal/task/task_stack_arm.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ package task

import "unsafe"

var systemStack uintptr

// calleeSavedRegs is the list of registers that must be saved and restored when
// switching between tasks. Also see task_stack_arm.S that relies on the exact
// layout of this struct.
Expand Down Expand Up @@ -44,18 +42,12 @@ func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
r.r5 = uintptr(args)
}

func (s *state) resume() {
swapTask(s.sp, &systemStack)
}

func (s *state) pause() {
newStack := systemStack
systemStack = 0
swapTask(newStack, &s.sp)
func (s *state) switchTo(current *Task) {
swapTask(s.sp, &current.state.sp)
}

// SystemStack returns the system stack pointer when called from a task stack.
// When called from the system stack, it returns 0.
func SystemStack() uintptr {
return systemStack
return mainTask.state.sp
}
14 changes: 3 additions & 11 deletions src/internal/task/task_stack_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ package task

import "unsafe"

var systemStack uintptr

// calleeSavedRegs is the list of registers that must be saved and restored when
// switching between tasks. Also see task_stack_arm64.S that relies on the exact
// layout of this struct.
Expand Down Expand Up @@ -47,18 +45,12 @@ func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
r.x20 = uintptr(args)
}

func (s *state) resume() {
swapTask(s.sp, &systemStack)
}

func (s *state) pause() {
newStack := systemStack
systemStack = 0
swapTask(newStack, &s.sp)
func (s *state) switchTo(current *Task) {
swapTask(s.sp, &current.state.sp)
}

// SystemStack returns the system stack pointer when called from a task stack.
// When called from the system stack, it returns 0.
func SystemStack() uintptr {
return systemStack
return mainTask.state.sp
}
6 changes: 0 additions & 6 deletions src/internal/task/task_stack_avr.S
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
.section .bss.tinygo_systemStack
.global tinygo_systemStack
.type tinygo_systemStack, %object
tinygo_systemStack:
.short 0

.section .text.tinygo_startTask
.global tinygo_startTask
.type tinygo_startTask, %function
Expand Down
15 changes: 3 additions & 12 deletions src/internal/task/task_stack_avr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ package task

import "unsafe"

//go:extern tinygo_systemStack
var systemStack uintptr

// calleeSavedRegs is the list of registers that must be saved and restored when
// switching between tasks. Also see task_stack_avr.S that relies on the exact
// layout of this struct.
Expand Down Expand Up @@ -50,18 +47,12 @@ func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
r.r4r5 = uintptr(args)
}

func (s *state) resume() {
swapTask(s.sp, &systemStack)
}

func (s *state) pause() {
newStack := systemStack
systemStack = 0
swapTask(newStack, &s.sp)
func (s *state) switchTo(current *Task) {
swapTask(s.sp, &current.state.sp)
}

// SystemStack returns the system stack pointer when called from a task stack.
// When called from the system stack, it returns 0.
func SystemStack() uintptr {
return systemStack
return mainTask.state.sp
}
Loading

0 comments on commit 50bfb0a

Please sign in to comment.