Skip to content

Commit 9f5066a

Browse files
aykevldeadprogram
authored andcommitted
runtime: use the tasks scheduler instead of coroutines
This results in smaller and likely more efficient code. It does require some architecture specific code for each architecture, but I've kept the amount of code as small as possible.
1 parent 3b24fed commit 9f5066a

File tree

12 files changed

+510
-8
lines changed

12 files changed

+510
-8
lines changed

compileopts/target.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,16 @@ func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) {
239239
// No target spec available. Use the default one, useful on most systems
240240
// with a regular OS.
241241
spec := TargetSpec{
242-
Triple: triple,
243-
GOOS: goos,
244-
GOARCH: goarch,
245-
BuildTags: []string{goos, goarch},
246-
Linker: "cc",
247-
CFlags: []string{"--target=" + triple},
248-
GDB: []string{"gdb"},
249-
PortReset: "false",
242+
Triple: triple,
243+
GOOS: goos,
244+
GOARCH: goarch,
245+
BuildTags: []string{goos, goarch},
246+
Scheduler: "tasks",
247+
Linker: "cc",
248+
DefaultStackSize: 1024 * 64, // 64kB
249+
CFlags: []string{"--target=" + triple},
250+
GDB: []string{"gdb"},
251+
PortReset: "false",
250252
}
251253
if goos == "darwin" {
252254
spec.CFlags = append(spec.CFlags, "-isysroot", "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk")
@@ -256,6 +258,7 @@ func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) {
256258
}
257259
if goarch != "wasm" {
258260
spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/gc_"+goarch+".S")
261+
spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_stack_"+goarch+".S")
259262
}
260263
if goarch != runtime.GOARCH {
261264
// Some educated guesses as to how to invoke helper programs.

compiler/goroutine.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ func (b *builder) createGoInstruction(funcPtr llvm.Value, params []llvm.Value, p
3434
} else {
3535
// The stack size is fixed at compile time. By emitting it here as a
3636
// constant, it can be optimized.
37+
if b.DefaultStackSize == 0 {
38+
b.addError(pos, "default stack size for goroutines is not set")
39+
}
3740
stackSize = llvm.ConstInt(b.uintptrType, b.DefaultStackSize, false)
3841
}
3942
case "coroutines":

src/internal/task/task_stack_386.S

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
.section .text.tinygo_startTask
2+
.global tinygo_startTask
3+
.type tinygo_startTask, %function
4+
tinygo_startTask:
5+
.cfi_startproc
6+
// Small assembly stub for starting a goroutine. This is already run on the
7+
// new stack, with the callee-saved registers already loaded.
8+
// Most importantly, EBX contain the pc of the to-be-started function and
9+
// ESI contain the only argument it is given. Multiple arguments are packed
10+
// into one by storing them in a new allocation.
11+
12+
// Indicate to the unwinder that there is nothing to unwind, this is the
13+
// root frame. It avoids bogus extra frames in GDB.
14+
.cfi_undefined eip
15+
16+
// Set the first argument of the goroutine start wrapper, which contains all
17+
// the arguments.
18+
pushl %esi
19+
20+
// Branch to the "goroutine start" function.
21+
calll *%ebx
22+
23+
// Rebalance the stack (to undo the above push).
24+
addl $4, %esp
25+
26+
// After return, exit this goroutine. This is a tail call.
27+
jmp tinygo_pause
28+
.cfi_endproc
29+
30+
.global tinygo_swapTask
31+
.type tinygo_swapTask, %function
32+
tinygo_swapTask:
33+
// This function gets the following parameters:
34+
movl 4(%esp), %eax // newStack uintptr
35+
movl 8(%esp), %ecx // oldStack *uintptr
36+
// More information on the calling convention:
37+
// https://wiki.osdev.org/System_V_ABI#i386
38+
39+
// Save all callee-saved registers:
40+
pushl %ebp
41+
pushl %edi
42+
pushl %esi
43+
pushl %ebx
44+
45+
// Save the current stack pointer in oldStack.
46+
movl %esp, (%ecx)
47+
48+
// Switch to the new stack pointer.
49+
movl %eax, %esp
50+
51+
// Load saved register from the new stack.
52+
popl %ebx
53+
popl %esi
54+
popl %edi
55+
popl %ebp
56+
57+
// Return into the new task, as if tinygo_swapTask was a regular call.
58+
ret
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// +build scheduler.tasks,386
2+
3+
package task
4+
5+
import "unsafe"
6+
7+
var systemStack uintptr
8+
9+
// calleeSavedRegs is the list of registers that must be saved and restored when
10+
// switching between tasks. Also see task_stack_386.S that relies on the exact
11+
// layout of this struct.
12+
type calleeSavedRegs struct {
13+
ebx uintptr
14+
esi uintptr
15+
edi uintptr
16+
ebp uintptr
17+
18+
pc uintptr
19+
}
20+
21+
// archInit runs architecture-specific setup for the goroutine startup.
22+
func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
23+
// Store the initial sp for the startTask function (implemented in assembly).
24+
s.sp = uintptr(unsafe.Pointer(r))
25+
26+
// Initialize the registers.
27+
// These will be popped off of the stack on the first resume of the goroutine.
28+
29+
// Start the function at tinygo_startTask (defined in
30+
// src/internal/task/task_stack_386.S). This assembly code calls a function
31+
// (passed in EBX) with a single argument (passed in ESI). After the
32+
// function returns, it calls Pause().
33+
r.pc = uintptr(unsafe.Pointer(&startTask))
34+
35+
// Pass the function to call in EBX.
36+
// This function is a compiler-generated wrapper which loads arguments out
37+
// of a struct pointer. See createGoroutineStartWrapper (defined in
38+
// compiler/goroutine.go) for more information.
39+
r.ebx = fn
40+
41+
// Pass the pointer to the arguments struct in ESI.
42+
r.esi = uintptr(args)
43+
}
44+
45+
func (s *state) resume() {
46+
swapTask(s.sp, &systemStack)
47+
}
48+
49+
func (s *state) pause() {
50+
newStack := systemStack
51+
systemStack = 0
52+
swapTask(newStack, &s.sp)
53+
}
54+
55+
// SystemStack returns the system stack pointer when called from a task stack.
56+
// When called from the system stack, it returns 0.
57+
func SystemStack() uintptr {
58+
return systemStack
59+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#ifdef __MACH__ // Darwin
2+
.global _tinygo_startTask
3+
_tinygo_startTask:
4+
#else // Linux etc
5+
.section .text.tinygo_startTask
6+
.global tinygo_startTask
7+
tinygo_startTask:
8+
#endif
9+
.cfi_startproc
10+
// Small assembly stub for starting a goroutine. This is already run on the
11+
// new stack, with the callee-saved registers already loaded.
12+
// Most importantly, r12 contain the pc of the to-be-started function and
13+
// r13 contain the only argument it is given. Multiple arguments are packed
14+
// into one by storing them in a new allocation.
15+
16+
// Indicate to the unwinder that there is nothing to unwind, this is the
17+
// root frame. It avoids bogus extra frames in GDB like here:
18+
// #10 0x00000000004277b6 in <goroutine wrapper> () at [...]
19+
// #11 0x00000000004278f3 in tinygo_startTask () at [...]
20+
// #12 0x0000000000002030 in ?? ()
21+
// #13 0x0000000000000071 in ?? ()
22+
.cfi_undefined rip
23+
24+
// Set the first argument of the goroutine start wrapper, which contains all
25+
// the arguments.
26+
movq %r13, %rdi
27+
28+
// Branch to the "goroutine start" function.
29+
callq *%r12
30+
31+
// After return, exit this goroutine. This is a tail call.
32+
#ifdef __MACH__
33+
jmp _tinygo_pause
34+
#else
35+
jmp tinygo_pause
36+
#endif
37+
.cfi_endproc
38+
39+
#ifdef __MACH__ // Darwin
40+
.global _tinygo_swapTask
41+
_tinygo_swapTask:
42+
#else // Linux etc
43+
.global tinygo_swapTask
44+
.section .text.tinygo_swapTask
45+
tinygo_swapTask:
46+
#endif
47+
// This function gets the following parameters:
48+
// %rdi = newStack uintptr
49+
// %rsi = oldStack *uintptr
50+
51+
// Save all callee-saved registers:
52+
pushq %r15
53+
pushq %r14
54+
pushq %r13
55+
pushq %r12
56+
pushq %rbp
57+
pushq %rbx
58+
59+
// Save the current stack pointer in oldStack.
60+
movq %rsp, (%rsi)
61+
62+
// Switch to the new stack pointer.
63+
movq %rdi, %rsp
64+
65+
// Load saved register from the new stack.
66+
popq %rbx
67+
popq %rbp
68+
popq %r12
69+
popq %r13
70+
popq %r14
71+
popq %r15
72+
73+
// Return into the new task, as if tinygo_swapTask was a regular call.
74+
ret
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// +build scheduler.tasks,amd64
2+
3+
package task
4+
5+
import "unsafe"
6+
7+
var systemStack uintptr
8+
9+
// calleeSavedRegs is the list of registers that must be saved and restored when
10+
// switching between tasks. Also see task_stack_amd64.S that relies on the exact
11+
// layout of this struct.
12+
type calleeSavedRegs struct {
13+
rbx uintptr
14+
rbp uintptr
15+
r12 uintptr
16+
r13 uintptr
17+
r14 uintptr
18+
r15 uintptr
19+
20+
pc uintptr
21+
}
22+
23+
// archInit runs architecture-specific setup for the goroutine startup.
24+
func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
25+
// Store the initial sp for the startTask function (implemented in assembly).
26+
s.sp = uintptr(unsafe.Pointer(r))
27+
28+
// Initialize the registers.
29+
// These will be popped off of the stack on the first resume of the goroutine.
30+
31+
// Start the function at tinygo_startTask (defined in
32+
// src/internal/task/task_stack_amd64.S). This assembly code calls a
33+
// function (passed in r12) with a single argument (passed in r13). After
34+
// the function returns, it calls Pause().
35+
r.pc = uintptr(unsafe.Pointer(&startTask))
36+
37+
// Pass the function to call in r12.
38+
// This function is a compiler-generated wrapper which loads arguments out
39+
// of a struct pointer. See createGoroutineStartWrapper (defined in
40+
// compiler/goroutine.go) for more information.
41+
r.r12 = fn
42+
43+
// Pass the pointer to the arguments struct in r13.
44+
r.r13 = uintptr(args)
45+
}
46+
47+
func (s *state) resume() {
48+
swapTask(s.sp, &systemStack)
49+
}
50+
51+
func (s *state) pause() {
52+
newStack := systemStack
53+
systemStack = 0
54+
swapTask(newStack, &s.sp)
55+
}
56+
57+
// SystemStack returns the system stack pointer when called from a task stack.
58+
// When called from the system stack, it returns 0.
59+
func SystemStack() uintptr {
60+
return systemStack
61+
}

src/internal/task/task_stack_arm.S

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Only generate .debug_frame, don't generate .eh_frame.
2+
.cfi_sections .debug_frame
3+
4+
.section .text.tinygo_startTask
5+
.global tinygo_startTask
6+
.type tinygo_startTask, %function
7+
tinygo_startTask:
8+
.cfi_startproc
9+
// Small assembly stub for starting a goroutine. This is already run on the
10+
// new stack, with the callee-saved registers already loaded.
11+
// Most importantly, r4 contains the pc of the to-be-started function and r5
12+
// contains the only argument it is given. Multiple arguments are packed
13+
// into one by storing them in a new allocation.
14+
15+
// Indicate to the unwinder that there is nothing to unwind, this is the
16+
// root frame. It avoids the following (bogus) error message in GDB:
17+
// Backtrace stopped: previous frame identical to this frame (corrupt stack?)
18+
.cfi_undefined lr
19+
20+
// Set the first argument of the goroutine start wrapper, which contains all
21+
// the arguments.
22+
mov r0, r5
23+
24+
// Branch to the "goroutine start" function. By using blx instead of bx,
25+
// we'll return here instead of tail calling.
26+
blx r4
27+
28+
// After return, exit this goroutine. This is a tail call.
29+
bl tinygo_pause
30+
.cfi_endproc
31+
.size tinygo_startTask, .-tinygo_startTask
32+
33+
.global tinygo_swapTask
34+
.type tinygo_swapTask, %function
35+
tinygo_swapTask:
36+
// This function gets the following parameters:
37+
// r0 = newStack uintptr
38+
// r1 = oldStack *uintptr
39+
40+
// Save all callee-saved registers:
41+
push {r4-r11, lr}
42+
43+
// Save the current stack pointer in oldStack.
44+
str sp, [r1]
45+
46+
// Switch to the new stack pointer.
47+
mov sp, r0
48+
49+
// Load state from new task and branch to the previous position in the
50+
// program.
51+
pop {r4-r11, pc}

0 commit comments

Comments
 (0)