diff options
author | Ayke van Laethem <[email protected]> | 2020-09-25 15:31:50 +0200 |
---|---|---|
committer | Ron Evans <[email protected]> | 2021-05-09 17:40:13 +0200 |
commit | 9f5066aa6f1d67f161bc0d7520dc2f1bfa1f2b0d (patch) | |
tree | 69510a8f373164fdbe92c330f9e11a674447e15f /src/internal | |
parent | 3b24fedf922549297ae42c2da7e9292741334d76 (diff) | |
download | tinygo-9f5066aa6f1d67f161bc0d7520dc2f1bfa1f2b0d.tar.gz tinygo-9f5066aa6f1d67f161bc0d7520dc2f1bfa1f2b0d.zip |
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.
Diffstat (limited to 'src/internal')
-rw-r--r-- | src/internal/task/task_stack_386.S | 58 | ||||
-rw-r--r-- | src/internal/task/task_stack_386.go | 59 | ||||
-rw-r--r-- | src/internal/task/task_stack_amd64.S | 74 | ||||
-rw-r--r-- | src/internal/task/task_stack_amd64.go | 61 | ||||
-rw-r--r-- | src/internal/task/task_stack_arm.S | 51 | ||||
-rw-r--r-- | src/internal/task/task_stack_arm.go | 61 | ||||
-rw-r--r-- | src/internal/task/task_stack_arm64.S | 59 | ||||
-rw-r--r-- | src/internal/task/task_stack_arm64.go | 64 | ||||
-rw-r--r-- | src/internal/task/task_stack_cortexm.go | 6 |
9 files changed, 493 insertions, 0 deletions
diff --git a/src/internal/task/task_stack_386.S b/src/internal/task/task_stack_386.S new file mode 100644 index 000000000..c82213e98 --- /dev/null +++ b/src/internal/task/task_stack_386.S @@ -0,0 +1,58 @@ +.section .text.tinygo_startTask +.global tinygo_startTask +.type tinygo_startTask, %function +tinygo_startTask: + .cfi_startproc + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, EBX contain the pc of the to-be-started function and + // ESI contain the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Indicate to the unwinder that there is nothing to unwind, this is the + // root frame. It avoids bogus extra frames in GDB. + .cfi_undefined eip + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + pushl %esi + + // 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 + .cfi_endproc + +.global tinygo_swapTask +.type tinygo_swapTask, %function +tinygo_swapTask: + // This function gets the following parameters: + movl 4(%esp), %eax // newStack uintptr + movl 8(%esp), %ecx // oldStack *uintptr + // More information on the calling convention: + // https://wiki.osdev.org/System_V_ABI#i386 + + // Save all callee-saved registers: + pushl %ebp + pushl %edi + pushl %esi + pushl %ebx + + // Save the current stack pointer in oldStack. + movl %esp, (%ecx) + + // Switch to the new stack pointer. + movl %eax, %esp + + // Load saved register from the new stack. + popl %ebx + popl %esi + popl %edi + popl %ebp + + // Return into the new task, as if tinygo_swapTask was a regular call. + ret diff --git a/src/internal/task/task_stack_386.go b/src/internal/task/task_stack_386.go new file mode 100644 index 000000000..3f1cd3ee0 --- /dev/null +++ b/src/internal/task/task_stack_386.go @@ -0,0 +1,59 @@ +// +build scheduler.tasks,386 + +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. +type calleeSavedRegs struct { + ebx uintptr + esi uintptr + edi uintptr + ebp uintptr + + pc uintptr +} + +// archInit runs architecture-specific setup for the goroutine startup. +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { + // Store the initial sp for the startTask function (implemented in assembly). + s.sp = uintptr(unsafe.Pointer(r)) + + // Initialize the registers. + // These will be popped off of the stack on the first resume of the goroutine. + + // Start the function at tinygo_startTask (defined in + // src/internal/task/task_stack_386.S). This assembly code calls a function + // (passed in EBX) with a single argument (passed in ESI). After the + // function returns, it calls Pause(). + r.pc = uintptr(unsafe.Pointer(&startTask)) + + // Pass the function to call in EBX. + // This function is a compiler-generated wrapper which loads arguments out + // of a struct pointer. See createGoroutineStartWrapper (defined in + // compiler/goroutine.go) for more information. + r.ebx = fn + + // Pass the pointer to the arguments struct in ESI. + r.esi = uintptr(args) +} + +func (s *state) resume() { + swapTask(s.sp, &systemStack) +} + +func (s *state) pause() { + newStack := systemStack + systemStack = 0 + swapTask(newStack, &s.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 +} diff --git a/src/internal/task/task_stack_amd64.S b/src/internal/task/task_stack_amd64.S new file mode 100644 index 000000000..44b5f065b --- /dev/null +++ b/src/internal/task/task_stack_amd64.S @@ -0,0 +1,74 @@ +#ifdef __MACH__ // Darwin +.global _tinygo_startTask +_tinygo_startTask: +#else // Linux etc +.section .text.tinygo_startTask +.global tinygo_startTask +tinygo_startTask: +#endif + .cfi_startproc + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, r12 contain the pc of the to-be-started function and + // r13 contain the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Indicate to the unwinder that there is nothing to unwind, this is the + // root frame. It avoids bogus extra frames in GDB like here: + // #10 0x00000000004277b6 in <goroutine wrapper> () at [...] + // #11 0x00000000004278f3 in tinygo_startTask () at [...] + // #12 0x0000000000002030 in ?? () + // #13 0x0000000000000071 in ?? () + .cfi_undefined rip + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + movq %r13, %rdi + + // Branch to the "goroutine start" function. + callq *%r12 + + // After return, exit this goroutine. This is a tail call. + #ifdef __MACH__ + jmp _tinygo_pause + #else + jmp tinygo_pause + #endif + .cfi_endproc + +#ifdef __MACH__ // Darwin +.global _tinygo_swapTask +_tinygo_swapTask: +#else // Linux etc +.global tinygo_swapTask +.section .text.tinygo_swapTask +tinygo_swapTask: +#endif + // This function gets the following parameters: + // %rdi = newStack uintptr + // %rsi = oldStack *uintptr + + // Save all callee-saved registers: + pushq %r15 + pushq %r14 + pushq %r13 + pushq %r12 + pushq %rbp + pushq %rbx + + // Save the current stack pointer in oldStack. + movq %rsp, (%rsi) + + // Switch to the new stack pointer. + movq %rdi, %rsp + + // Load saved register from the new stack. + popq %rbx + popq %rbp + popq %r12 + popq %r13 + popq %r14 + popq %r15 + + // Return into the new task, as if tinygo_swapTask was a regular call. + ret diff --git a/src/internal/task/task_stack_amd64.go b/src/internal/task/task_stack_amd64.go new file mode 100644 index 000000000..7f1d132ed --- /dev/null +++ b/src/internal/task/task_stack_amd64.go @@ -0,0 +1,61 @@ +// +build scheduler.tasks,amd64 + +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. +type calleeSavedRegs struct { + rbx uintptr + rbp uintptr + r12 uintptr + r13 uintptr + r14 uintptr + r15 uintptr + + pc uintptr +} + +// archInit runs architecture-specific setup for the goroutine startup. +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { + // Store the initial sp for the startTask function (implemented in assembly). + s.sp = uintptr(unsafe.Pointer(r)) + + // Initialize the registers. + // These will be popped off of the stack on the first resume of the goroutine. + + // Start the function at tinygo_startTask (defined in + // src/internal/task/task_stack_amd64.S). This assembly code calls a + // function (passed in r12) with a single argument (passed in r13). After + // the function returns, it calls Pause(). + r.pc = uintptr(unsafe.Pointer(&startTask)) + + // Pass the function to call in r12. + // This function is a compiler-generated wrapper which loads arguments out + // of a struct pointer. See createGoroutineStartWrapper (defined in + // compiler/goroutine.go) for more information. + r.r12 = fn + + // Pass the pointer to the arguments struct in r13. + r.r13 = uintptr(args) +} + +func (s *state) resume() { + swapTask(s.sp, &systemStack) +} + +func (s *state) pause() { + newStack := systemStack + systemStack = 0 + swapTask(newStack, &s.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 +} diff --git a/src/internal/task/task_stack_arm.S b/src/internal/task/task_stack_arm.S new file mode 100644 index 000000000..3a1e3f0ae --- /dev/null +++ b/src/internal/task/task_stack_arm.S @@ -0,0 +1,51 @@ +// Only generate .debug_frame, don't generate .eh_frame. +.cfi_sections .debug_frame + +.section .text.tinygo_startTask +.global tinygo_startTask +.type tinygo_startTask, %function +tinygo_startTask: + .cfi_startproc + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, r4 contains the pc of the to-be-started function and r5 + // contains the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Indicate to the unwinder that there is nothing to unwind, this is the + // root frame. It avoids the following (bogus) error message in GDB: + // Backtrace stopped: previous frame identical to this frame (corrupt stack?) + .cfi_undefined lr + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + mov r0, r5 + + // Branch to the "goroutine start" function. By using blx instead of bx, + // we'll return here instead of tail calling. + blx r4 + + // After return, exit this goroutine. This is a tail call. + bl tinygo_pause + .cfi_endproc +.size tinygo_startTask, .-tinygo_startTask + +.global tinygo_swapTask +.type tinygo_swapTask, %function +tinygo_swapTask: + // This function gets the following parameters: + // r0 = newStack uintptr + // r1 = oldStack *uintptr + + // Save all callee-saved registers: + push {r4-r11, lr} + + // Save the current stack pointer in oldStack. + str sp, [r1] + + // Switch to the new stack pointer. + mov sp, r0 + + // Load state from new task and branch to the previous position in the + // program. + pop {r4-r11, pc} diff --git a/src/internal/task/task_stack_arm.go b/src/internal/task/task_stack_arm.go new file mode 100644 index 000000000..acd63ece8 --- /dev/null +++ b/src/internal/task/task_stack_arm.go @@ -0,0 +1,61 @@ +// +build scheduler.tasks,arm,!cortexm,!avr,!xtensa + +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. +type calleeSavedRegs struct { + r4 uintptr + r5 uintptr + r6 uintptr + r7 uintptr + r8 uintptr + r9 uintptr + r10 uintptr + r11 uintptr + + pc uintptr +} + +// archInit runs architecture-specific setup for the goroutine startup. +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { + // Store the initial sp for the startTask function (implemented in assembly). + s.sp = uintptr(unsafe.Pointer(r)) + + // Initialize the registers. + // These will be popped off of the stack on the first resume of the goroutine. + + // Start the function at tinygo_startTask (defined in src/internal/task/task_stack_arm.S). + // This assembly code calls a function (passed in r4) with a single argument + // (passed in r5). After the function returns, it calls Pause(). + r.pc = uintptr(unsafe.Pointer(&startTask)) + + // Pass the function to call in r4. + // This function is a compiler-generated wrapper which loads arguments out of a struct pointer. + // See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information. + r.r4 = fn + + // Pass the pointer to the arguments struct in r5. + r.r5 = uintptr(args) +} + +func (s *state) resume() { + swapTask(s.sp, &systemStack) +} + +func (s *state) pause() { + newStack := systemStack + systemStack = 0 + swapTask(newStack, &s.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 +} diff --git a/src/internal/task/task_stack_arm64.S b/src/internal/task/task_stack_arm64.S new file mode 100644 index 000000000..29042860f --- /dev/null +++ b/src/internal/task/task_stack_arm64.S @@ -0,0 +1,59 @@ +.section .text.tinygo_startTask +.global tinygo_startTask +.type tinygo_startTask, %function +tinygo_startTask: + .cfi_startproc + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, x19 contains the pc of the to-be-started function and + // x20 contains the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Indicate to the unwinder that there is nothing to unwind, this is the + // root frame. It avoids the following (bogus) error message in GDB: + // Backtrace stopped: previous frame identical to this frame (corrupt stack?) + .cfi_undefined lr + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + mov x0, x20 + + // Branch to the "goroutine start" function. By using blx instead of bx, + // we'll return here instead of tail calling. + blr x19 + + // After return, exit this goroutine. This is a tail call. + b tinygo_pause + .cfi_endproc +.size tinygo_startTask, .-tinygo_startTask + +.global tinygo_swapTask +.type tinygo_swapTask, %function +tinygo_swapTask: + // This function gets the following parameters: + // x0 = newStack uintptr + // x1 = oldStack *uintptr + + // Save all callee-saved registers: + stp x19, x20, [sp, #-96]! + stp x21, x22, [sp, #16] + stp x23, x24, [sp, #32] + stp x25, x26, [sp, #48] + stp x27, x28, [sp, #64] + stp x29, x30, [sp, #80] + + // Save the current stack pointer in oldStack. + mov x8, sp + str x8, [x1] + + // Switch to the new stack pointer. + mov sp, x0 + + // Restore stack state and return. + ldp x29, x30, [sp, #80] + ldp x27, x28, [sp, #64] + ldp x25, x26, [sp, #48] + ldp x23, x24, [sp, #32] + ldp x21, x22, [sp, #16] + ldp x19, x20, [sp], #96 + ret diff --git a/src/internal/task/task_stack_arm64.go b/src/internal/task/task_stack_arm64.go new file mode 100644 index 000000000..d65c30c49 --- /dev/null +++ b/src/internal/task/task_stack_arm64.go @@ -0,0 +1,64 @@ +// +build scheduler.tasks,arm64 + +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. +type calleeSavedRegs struct { + x19 uintptr + x20 uintptr + x21 uintptr + x22 uintptr + x23 uintptr + x24 uintptr + x25 uintptr + x26 uintptr + x27 uintptr + x28 uintptr + x29 uintptr + + pc uintptr // aka x30 aka LR +} + +// archInit runs architecture-specific setup for the goroutine startup. +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { + // Store the initial sp for the startTask function (implemented in assembly). + s.sp = uintptr(unsafe.Pointer(r)) + + // Initialize the registers. + // These will be popped off of the stack on the first resume of the goroutine. + + // Start the function at tinygo_startTask (defined in src/internal/task/task_stack_arm64.S). + // This assembly code calls a function (passed in x19) with a single argument + // (passed in x20). After the function returns, it calls Pause(). + r.pc = uintptr(unsafe.Pointer(&startTask)) + + // Pass the function to call in x19. + // This function is a compiler-generated wrapper which loads arguments out of a struct pointer. + // See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information. + r.x19 = fn + + // Pass the pointer to the arguments struct in x20. + r.x20 = uintptr(args) +} + +func (s *state) resume() { + swapTask(s.sp, &systemStack) +} + +func (s *state) pause() { + newStack := systemStack + systemStack = 0 + swapTask(newStack, &s.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 +} diff --git a/src/internal/task/task_stack_cortexm.go b/src/internal/task/task_stack_cortexm.go index eb3fe1827..1d9969796 100644 --- a/src/internal/task/task_stack_cortexm.go +++ b/src/internal/task/task_stack_cortexm.go @@ -2,6 +2,12 @@ package task +// Note that this is almost the same as task_stack_arm.go, but it uses the MSP +// register to store the system stack pointer instead of a global variable. The +// big advantage of this is that interrupts always execute with MSP (and not +// PSP, which is used for goroutines) so that goroutines do not need extra stack +// space for interrupts. + import ( "device/arm" "unsafe" |