mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-12 23:13:40 +00:00
compiler,runtime: implement stack-based scheduler
This scheduler is intended to live along the (stackless) coroutine based scheduler which is needed for WebAssembly and unsupported platforms. The stack based scheduler is somewhat simpler in implementation as it does not require full program transform passes and supports things like function pointers and interface methods out of the box with no changes. Code size is reduced in most cases, even in the case where no scheduler scheduler is used at all. I'm not exactly sure why but these changes likely allowed some further optimizations somewhere. Even RAM is slightly reduced, perhaps some global was elminated in the process as well.
This commit is contained in:
committed by
Ron Evans
parent
61f711ef26
commit
542135c357
+71
-124
@@ -1,63 +1,28 @@
|
||||
package runtime
|
||||
|
||||
// This file implements the Go scheduler using coroutines.
|
||||
// A goroutine contains a whole stack. A coroutine is just a single function.
|
||||
// How do we use coroutines for goroutines, then?
|
||||
// * Every function that contains a blocking call (like sleep) is marked
|
||||
// blocking, and all it's parents (callers) are marked blocking as well
|
||||
// transitively until the root (main.main or a go statement).
|
||||
// * A blocking function that calls a non-blocking function is called as
|
||||
// usual.
|
||||
// * A blocking function that calls a blocking function passes its own
|
||||
// coroutine handle as a parameter to the subroutine. When the subroutine
|
||||
// returns, it will re-insert the parent into the scheduler.
|
||||
// Note that a goroutine is generally called a 'task' for brevity and because
|
||||
// that's the more common term among RTOSes. But a goroutine and a task are
|
||||
// basically the same thing. Although, the code often uses the word 'task' to
|
||||
// refer to both a coroutine and a goroutine, as most of the scheduler doesn't
|
||||
// care about the difference.
|
||||
// This file implements the TinyGo scheduler. This scheduler is a very simple
|
||||
// cooperative round robin scheduler, with a runqueue that contains a linked
|
||||
// list of goroutines (tasks) that should be run next, in order of when they
|
||||
// were added to the queue (first-in, first-out). It also contains a sleep queue
|
||||
// with sleeping goroutines in order of when they should be re-activated.
|
||||
//
|
||||
// For more background on coroutines in LLVM:
|
||||
// https://llvm.org/docs/Coroutines.html
|
||||
// The scheduler is used both for the coroutine based scheduler and for the task
|
||||
// based scheduler (see compiler/goroutine-lowering.go for a description). In
|
||||
// both cases, the 'task' type is used to represent one goroutine. In the case
|
||||
// of the task based scheduler, it literally is the goroutine itself: a pointer
|
||||
// to the bottom of the stack where some important fields are kept. In the case
|
||||
// of the coroutine-based scheduler, it is the coroutine pointer (a *i8 in
|
||||
// LLVM).
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
import "unsafe"
|
||||
|
||||
const schedulerDebug = false
|
||||
|
||||
// A coroutine instance, wrapped here to provide some type safety. The value
|
||||
// must not be used directly, it is meant to be used as an opaque *i8 in LLVM.
|
||||
type coroutine uint8
|
||||
|
||||
//go:export llvm.coro.resume
|
||||
func (t *coroutine) resume()
|
||||
|
||||
//go:export llvm.coro.destroy
|
||||
func (t *coroutine) destroy()
|
||||
|
||||
//go:export llvm.coro.done
|
||||
func (t *coroutine) done() bool
|
||||
|
||||
//go:export llvm.coro.promise
|
||||
func (t *coroutine) _promise(alignment int32, from bool) unsafe.Pointer
|
||||
|
||||
// Get the promise belonging to a task.
|
||||
func (t *coroutine) promise() *taskState {
|
||||
return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false))
|
||||
}
|
||||
|
||||
func makeGoroutine(uintptr) uintptr
|
||||
|
||||
// Compiler stub to get the current goroutine. Calls to this function are
|
||||
// removed in the goroutine lowering pass.
|
||||
func getCoroutine() *coroutine
|
||||
|
||||
// State/promise of a task. Internally represented as:
|
||||
// State of a task. Internally represented as:
|
||||
//
|
||||
// {i8* next, i1 commaOk, i32/i64 data}
|
||||
// {i8* next, i8* ptr, i32/i64 data}
|
||||
type taskState struct {
|
||||
next *coroutine
|
||||
next *task
|
||||
ptr unsafe.Pointer
|
||||
data uint
|
||||
}
|
||||
@@ -67,35 +32,42 @@ type taskState struct {
|
||||
// TODO: runqueueFront can be removed by making the run queue a circular linked
|
||||
// list. The runqueueBack will simply refer to the front in the 'next' pointer.
|
||||
var (
|
||||
runqueueFront *coroutine
|
||||
runqueueBack *coroutine
|
||||
sleepQueue *coroutine
|
||||
runqueueFront *task
|
||||
runqueueBack *task
|
||||
sleepQueue *task
|
||||
sleepQueueBaseTime timeUnit
|
||||
)
|
||||
|
||||
// Simple logging, for debugging.
|
||||
func scheduleLog(msg string) {
|
||||
if schedulerDebug {
|
||||
println(msg)
|
||||
println("---", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple logging with a task pointer, for debugging.
|
||||
func scheduleLogTask(msg string, t *coroutine) {
|
||||
func scheduleLogTask(msg string, t *task) {
|
||||
if schedulerDebug {
|
||||
println(msg, t)
|
||||
println("---", msg, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple logging with a channel and task pointer.
|
||||
func scheduleLogChan(msg string, ch *channel, t *task) {
|
||||
if schedulerDebug {
|
||||
println("---", msg, ch, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the task to sleep for a given time.
|
||||
//
|
||||
// This is a compiler intrinsic.
|
||||
func sleepTask(caller *coroutine, duration int64) {
|
||||
func sleepTask(caller *task, duration int64) {
|
||||
if schedulerDebug {
|
||||
println(" set sleep:", caller, uint(duration/tickMicros))
|
||||
}
|
||||
promise := caller.promise()
|
||||
promise.data = uint(duration / tickMicros) // TODO: longer durations
|
||||
state := caller.state()
|
||||
state.data = uint(duration / tickMicros) // TODO: longer durations
|
||||
addSleepTask(caller)
|
||||
}
|
||||
|
||||
@@ -103,83 +75,58 @@ func sleepTask(caller *coroutine, duration int64) {
|
||||
//
|
||||
// This is a compiler intrinsic, and is called from a callee to reactivate the
|
||||
// caller.
|
||||
func activateTask(task *coroutine) {
|
||||
if task == nil {
|
||||
func activateTask(t *task) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
scheduleLogTask(" set runnable:", task)
|
||||
runqueuePushBack(task)
|
||||
scheduleLogTask(" set runnable:", t)
|
||||
runqueuePushBack(t)
|
||||
}
|
||||
|
||||
// getTaskPromisePtr is a helper function to set the current .ptr field of a
|
||||
// coroutine promise.
|
||||
func setTaskPromisePtr(task *coroutine, value unsafe.Pointer) {
|
||||
task.promise().ptr = value
|
||||
}
|
||||
|
||||
// getTaskPromisePtr is a helper function to get the current .ptr field from a
|
||||
// coroutine promise.
|
||||
func getTaskPromisePtr(task *coroutine) unsafe.Pointer {
|
||||
if task == nil {
|
||||
blockingPanic()
|
||||
}
|
||||
return task.promise().ptr
|
||||
}
|
||||
|
||||
// getTaskPromiseData is a helper function to get the current .data field of a
|
||||
// coroutine promise.
|
||||
func getTaskPromiseData(task *coroutine) uint {
|
||||
return task.promise().data
|
||||
// getTaskStateData is a helper function to get the current .data field of the
|
||||
// goroutine state.
|
||||
func getTaskStateData(t *task) uint {
|
||||
return t.state().data
|
||||
}
|
||||
|
||||
// Add this task to the end of the run queue. May also destroy the task if it's
|
||||
// done.
|
||||
func runqueuePushBack(t *coroutine) {
|
||||
if t.done() {
|
||||
scheduleLogTask(" destroy task:", t)
|
||||
t.destroy()
|
||||
return
|
||||
}
|
||||
func runqueuePushBack(t *task) {
|
||||
if schedulerDebug {
|
||||
if t.promise().next != nil {
|
||||
if t.state().next != nil {
|
||||
panic("runtime: runqueuePushBack: expected next task to be nil")
|
||||
}
|
||||
}
|
||||
if runqueueBack == nil { // empty runqueue
|
||||
scheduleLogTask(" add to runqueue front:", t)
|
||||
runqueueBack = t
|
||||
runqueueFront = t
|
||||
} else {
|
||||
scheduleLogTask(" add to runqueue back:", t)
|
||||
lastTaskPromise := runqueueBack.promise()
|
||||
lastTaskPromise.next = t
|
||||
lastTaskState := runqueueBack.state()
|
||||
lastTaskState.next = t
|
||||
runqueueBack = t
|
||||
}
|
||||
}
|
||||
|
||||
// Get a task from the front of the run queue. Returns nil if there is none.
|
||||
func runqueuePopFront() *coroutine {
|
||||
func runqueuePopFront() *task {
|
||||
t := runqueueFront
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if schedulerDebug {
|
||||
println(" runqueuePopFront:", t)
|
||||
}
|
||||
promise := t.promise()
|
||||
runqueueFront = promise.next
|
||||
state := t.state()
|
||||
runqueueFront = state.next
|
||||
if runqueueFront == nil {
|
||||
// Runqueue is empty now.
|
||||
runqueueBack = nil
|
||||
}
|
||||
promise.next = nil
|
||||
state.next = nil
|
||||
return t
|
||||
}
|
||||
|
||||
// Add this task to the sleep queue, assuming its state is set to sleeping.
|
||||
func addSleepTask(t *coroutine) {
|
||||
func addSleepTask(t *task) {
|
||||
if schedulerDebug {
|
||||
if t.promise().next != nil {
|
||||
if t.state().next != nil {
|
||||
panic("runtime: addSleepTask: expected next task to be nil")
|
||||
}
|
||||
}
|
||||
@@ -192,14 +139,14 @@ func addSleepTask(t *coroutine) {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure promise.data is relative to the queue time base.
|
||||
promise := t.promise()
|
||||
// Make sure state.data is relative to the queue time base.
|
||||
state := t.state()
|
||||
|
||||
// Insert at front of sleep queue.
|
||||
if promise.data < sleepQueue.promise().data {
|
||||
if state.data < sleepQueue.state().data {
|
||||
scheduleLog(" -> sleep at start")
|
||||
sleepQueue.promise().data -= promise.data
|
||||
promise.next = sleepQueue
|
||||
sleepQueue.state().data -= state.data
|
||||
state.next = sleepQueue
|
||||
sleepQueue = t
|
||||
return
|
||||
}
|
||||
@@ -207,20 +154,20 @@ func addSleepTask(t *coroutine) {
|
||||
// Add to sleep queue (in the middle or at the end).
|
||||
queueIndex := sleepQueue
|
||||
for {
|
||||
promise.data -= queueIndex.promise().data
|
||||
if queueIndex.promise().next == nil || queueIndex.promise().data > promise.data {
|
||||
if queueIndex.promise().next == nil {
|
||||
state.data -= queueIndex.state().data
|
||||
if queueIndex.state().next == nil || queueIndex.state().data > state.data {
|
||||
if queueIndex.state().next == nil {
|
||||
scheduleLog(" -> sleep at end")
|
||||
promise.next = nil
|
||||
state.next = nil
|
||||
} else {
|
||||
scheduleLog(" -> sleep in middle")
|
||||
promise.next = queueIndex.promise().next
|
||||
promise.next.promise().data -= promise.data
|
||||
state.next = queueIndex.state().next
|
||||
state.next.state().data -= state.data
|
||||
}
|
||||
queueIndex.promise().next = t
|
||||
queueIndex.state().next = t
|
||||
break
|
||||
}
|
||||
queueIndex = queueIndex.promise().next
|
||||
queueIndex = queueIndex.state().next
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,18 +175,19 @@ func addSleepTask(t *coroutine) {
|
||||
func scheduler() {
|
||||
// Main scheduler loop.
|
||||
for {
|
||||
scheduleLog("\n schedule")
|
||||
scheduleLog("")
|
||||
scheduleLog(" schedule")
|
||||
now := ticks()
|
||||
|
||||
// Add tasks that are done sleeping to the end of the runqueue so they
|
||||
// will be executed soon.
|
||||
if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.promise().data) {
|
||||
if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.state().data) {
|
||||
t := sleepQueue
|
||||
scheduleLogTask(" awake:", t)
|
||||
promise := t.promise()
|
||||
sleepQueueBaseTime += timeUnit(promise.data)
|
||||
sleepQueue = promise.next
|
||||
promise.next = nil
|
||||
state := t.state()
|
||||
sleepQueueBaseTime += timeUnit(state.data)
|
||||
sleepQueue = state.next
|
||||
state.next = nil
|
||||
runqueuePushBack(t)
|
||||
}
|
||||
|
||||
@@ -253,7 +201,7 @@ func scheduler() {
|
||||
scheduleLog(" no tasks left!")
|
||||
return
|
||||
}
|
||||
timeLeft := timeUnit(sleepQueue.promise().data) - (now - sleepQueueBaseTime)
|
||||
timeLeft := timeUnit(sleepQueue.state().data) - (now - sleepQueueBaseTime)
|
||||
if schedulerDebug {
|
||||
println(" sleeping...", sleepQueue, uint(timeLeft))
|
||||
}
|
||||
@@ -268,7 +216,6 @@ func scheduler() {
|
||||
}
|
||||
|
||||
// Run the given task.
|
||||
scheduleLog(" <- runqueuePopFront")
|
||||
scheduleLogTask(" run:", t)
|
||||
t.resume()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user