Skip to content

Commit 11b818e

Browse files
committed
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.
1 parent fd3309a commit 11b818e

17 files changed

+702
-224
lines changed

compiler/channel.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (c *Compiler) emitChanRecv(frame *Frame, unop *ssa.UnOp) llvm.Value {
6969
c.emitLifetimeEnd(valueAllocaCast, valueAllocaSize)
7070

7171
if unop.CommaOk {
72-
commaOk := c.createRuntimeCall("getTaskPromiseData", []llvm.Value{coroutine}, "chan.commaOk.wide")
72+
commaOk := c.createRuntimeCall("getTaskStateData", []llvm.Value{coroutine}, "chan.commaOk.wide")
7373
commaOk = c.builder.CreateTrunc(commaOk, c.ctx.Int1Type(), "chan.commaOk")
7474
tuple := llvm.Undef(c.ctx.StructType([]llvm.Type{valueType, c.ctx.Int1Type()}, false))
7575
tuple = c.builder.CreateInsertValue(tuple, received, 0, "")
@@ -95,7 +95,7 @@ func (c *Compiler) emitSelect(frame *Frame, expr *ssa.Select) llvm.Value {
9595
if expr.Blocking {
9696
// Blocks forever:
9797
// select {}
98-
c.createRuntimeCall("deadlockStub", nil, "")
98+
c.createRuntimeCall("deadlock", nil, "")
9999
return llvm.Undef(llvmType)
100100
} else {
101101
// No-op:

compiler/compiler.go

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ func init() {
3030
// The TinyGo import path.
3131
const tinygoPath = "github.com/tinygo-org/tinygo"
3232

33+
// functionsUsedInTransform is a list of function symbols that may be used
34+
// during TinyGo optimization passes so they have to be marked as external
35+
// linkage until all TinyGo passes have finished.
36+
var functionsUsedInTransforms = []string{
37+
"runtime.alloc",
38+
"runtime.free",
39+
"runtime.sleepTask",
40+
"runtime.setTaskStatePtr",
41+
"runtime.getTaskStatePtr",
42+
"runtime.activateTask",
43+
"runtime.scheduler",
44+
"runtime.startGoroutine",
45+
}
46+
3347
// Configure the compiler.
3448
type Config struct {
3549
Triple string // LLVM target triple, e.g. x86_64-unknown-linux-gnu (empty string means default)
@@ -38,6 +52,7 @@ type Config struct {
3852
GOOS string //
3953
GOARCH string //
4054
GC string // garbage collection strategy
55+
Scheduler string // scheduler implementation ("coroutines" or "tasks")
4156
PanicStrategy string // panic strategy ("print" or "trap")
4257
CFlags []string // cflags to pass to cgo
4358
LDFlags []string // ldflags to pass to cgo
@@ -173,6 +188,17 @@ func (c *Compiler) selectGC() string {
173188
return "conservative"
174189
}
175190

191+
// selectScheduler picks an appropriate scheduler for the target if none was
192+
// given.
193+
func (c *Compiler) selectScheduler() string {
194+
if c.Scheduler != "" {
195+
// A scheduler was specified in the target description.
196+
return c.Scheduler
197+
}
198+
// Fall back to coroutines, which are supported everywhere.
199+
return "coroutines"
200+
}
201+
176202
// Compile the given package path or .go file path. Return an error when this
177203
// fails (in any stage).
178204
func (c *Compiler) Compile(mainPath string) []error {
@@ -189,6 +215,7 @@ func (c *Compiler) Compile(mainPath string) []error {
189215
if err != nil {
190216
return []error{err}
191217
}
218+
buildTags := append([]string{"tinygo", "gc." + c.selectGC(), "scheduler." + c.selectScheduler()}, c.BuildTags...)
192219
lprogram := &loader.Program{
193220
Build: &build.Context{
194221
GOARCH: c.GOARCH,
@@ -198,7 +225,7 @@ func (c *Compiler) Compile(mainPath string) []error {
198225
CgoEnabled: true,
199226
UseAllFiles: false,
200227
Compiler: "gc", // must be one of the recognized compilers
201-
BuildTags: append([]string{"tinygo", "gc." + c.selectGC()}, c.BuildTags...),
228+
BuildTags: buildTags,
202229
},
203230
OverlayBuild: &build.Context{
204231
GOARCH: c.GOARCH,
@@ -208,7 +235,7 @@ func (c *Compiler) Compile(mainPath string) []error {
208235
CgoEnabled: true,
209236
UseAllFiles: false,
210237
Compiler: "gc", // must be one of the recognized compilers
211-
BuildTags: append([]string{"tinygo", "gc." + c.selectGC()}, c.BuildTags...),
238+
BuildTags: buildTags,
212239
},
213240
OverlayPath: func(path string) string {
214241
// Return the (overlay) import path when it should be overlaid, and
@@ -335,13 +362,15 @@ func (c *Compiler) Compile(mainPath string) []error {
335362
// would be optimized away.
336363
realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main")
337364
realMain.SetLinkage(llvm.ExternalLinkage) // keep alive until goroutine lowering
338-
c.mod.NamedFunction("runtime.alloc").SetLinkage(llvm.ExternalLinkage)
339-
c.mod.NamedFunction("runtime.free").SetLinkage(llvm.ExternalLinkage)
340-
c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.ExternalLinkage)
341-
c.mod.NamedFunction("runtime.setTaskPromisePtr").SetLinkage(llvm.ExternalLinkage)
342-
c.mod.NamedFunction("runtime.getTaskPromisePtr").SetLinkage(llvm.ExternalLinkage)
343-
c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.ExternalLinkage)
344-
c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.ExternalLinkage)
365+
366+
// Make sure these functions are kept in tact during TinyGo transformation passes.
367+
for _, name := range functionsUsedInTransforms {
368+
fn := c.mod.NamedFunction(name)
369+
if fn.IsNil() {
370+
continue
371+
}
372+
fn.SetLinkage(llvm.ExternalLinkage)
373+
}
345374

346375
// Load some attributes
347376
getAttr := func(attrName string) llvm.Attribute {
@@ -1041,25 +1070,21 @@ func (c *Compiler) parseInstr(frame *Frame, instr ssa.Instruction) {
10411070
}
10421071
calleeFn := c.ir.GetFunction(callee)
10431072

1044-
// Mark this function as a 'go' invocation and break invalid
1045-
// interprocedural optimizations. For example, heap-to-stack
1046-
// transformations are not sound as goroutines can outlive their parent.
1047-
calleeType := calleeFn.LLVMFn.Type()
1048-
calleeValue := c.builder.CreatePtrToInt(calleeFn.LLVMFn, c.uintptrType, "")
1049-
calleeValue = c.createRuntimeCall("makeGoroutine", []llvm.Value{calleeValue}, "")
1050-
calleeValue = c.builder.CreateIntToPtr(calleeValue, calleeType, "")
1051-
10521073
// Get all function parameters to pass to the goroutine.
10531074
var params []llvm.Value
10541075
for _, param := range instr.Call.Args {
10551076
params = append(params, c.getValue(frame, param))
10561077
}
1057-
if !calleeFn.IsExported() {
1078+
if !calleeFn.IsExported() && c.selectScheduler() != "tasks" {
1079+
// For coroutine scheduling, this is only required when calling an
1080+
// external function.
1081+
// For tasks, because all params are stored in a single object, no
1082+
// unnecessary parameters should be stored anyway.
10581083
params = append(params, llvm.Undef(c.i8ptrType)) // context parameter
10591084
params = append(params, llvm.Undef(c.i8ptrType)) // parent coroutine handle
10601085
}
10611086

1062-
c.createCall(calleeValue, params, "")
1087+
c.emitStartGoroutine(calleeFn.LLVMFn, params)
10631088
case *ssa.If:
10641089
cond := c.getValue(frame, instr.Cond)
10651090
block := instr.Block()

compiler/goroutine-lowering.go

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
package compiler
22

3+
// This file implements lowering for the goroutine scheduler. There are two
4+
// scheduler implementations, one based on tasks (like RTOSes and the main Go
5+
// runtime) and one based on a coroutine compiler transformation. The task based
6+
// implementation requires very little work from the compiler but is not very
7+
// portable (in particular, it is very hard if not impossible to support on
8+
// WebAssembly). The coroutine based one requires a lot of work by the compiler
9+
// to implement, but can run virtually anywhere with a single scheduler
10+
// implementation.
11+
//
12+
// The below description is for the coroutine based scheduler.
13+
//
314
// This file lowers goroutine pseudo-functions into coroutines scheduled by a
415
// scheduler at runtime. It uses coroutine support in LLVM for this
516
// transformation: https://llvm.org/docs/Coroutines.html
@@ -62,7 +73,7 @@ package compiler
6273
// llvm.suspend(hdl) // suspend point
6374
// println("some other operation")
6475
// var i *int // allocate space on the stack for the return value
65-
// runtime.setTaskPromisePtr(hdl, &i) // store return value alloca in our coroutine promise
76+
// runtime.setTaskStatePtr(hdl, &i) // store return value alloca in our coroutine promise
6677
// bar(hdl) // await, pass a continuation (hdl) to bar
6778
// llvm.suspend(hdl) // suspend point, wait for the callee to re-activate
6879
// println("done", *i)
@@ -106,10 +117,65 @@ type asyncFunc struct {
106117
unreachableBlock llvm.BasicBlock
107118
}
108119

109-
// LowerGoroutines is a pass called during optimization that transforms the IR
110-
// into one where all blocking functions are turned into goroutines and blocking
111-
// calls into await calls.
120+
// LowerGoroutines performs some IR transformations necessary to support
121+
// goroutines. It does something different based on whether it uses the
122+
// coroutine or the tasks implementation of goroutines, and whether goroutines
123+
// are necessary at all.
112124
func (c *Compiler) LowerGoroutines() error {
125+
switch c.selectScheduler() {
126+
case "coroutines":
127+
return c.lowerCoroutines()
128+
case "tasks":
129+
return c.lowerTasks()
130+
default:
131+
panic("unknown scheduler type")
132+
}
133+
}
134+
135+
// lowerTasks starts the main goroutine and then runs the scheduler.
136+
// This is enough compiler-level transformation for the task-based scheduler.
137+
func (c *Compiler) lowerTasks() error {
138+
uses := getUses(c.mod.NamedFunction("runtime.callMain"))
139+
if len(uses) != 1 || uses[0].IsACallInst().IsNil() {
140+
panic("expected exactly 1 call of runtime.callMain, check the entry point")
141+
}
142+
mainCall := uses[0]
143+
144+
realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main")
145+
if len(getUses(c.mod.NamedFunction("runtime.startGoroutine"))) != 0 {
146+
// Program needs a scheduler. Start main.main as a goroutine and start
147+
// the scheduler.
148+
realMainWrapper := c.createGoroutineStartWrapper(realMain)
149+
c.builder.SetInsertPointBefore(mainCall)
150+
zero := llvm.ConstInt(c.uintptrType, 0, false)
151+
c.createRuntimeCall("startGoroutine", []llvm.Value{realMainWrapper, zero}, "")
152+
c.createRuntimeCall("scheduler", nil, "")
153+
} else {
154+
// Program doesn't need a scheduler. Call main.main directly.
155+
c.builder.SetInsertPointBefore(mainCall)
156+
params := []llvm.Value{
157+
llvm.Undef(c.i8ptrType), // unused context parameter
158+
llvm.Undef(c.i8ptrType), // unused coroutine handle
159+
}
160+
c.createCall(realMain, params, "")
161+
// runtime.Goexit isn't needed so let it be optimized away by
162+
// globalopt.
163+
c.mod.NamedFunction("runtime.Goexit").SetLinkage(llvm.InternalLinkage)
164+
}
165+
mainCall.EraseFromParentAsInstruction()
166+
167+
// main.main was set to external linkage during IR construction. Set it to
168+
// internal linkage to enable interprocedural optimizations.
169+
realMain.SetLinkage(llvm.InternalLinkage)
170+
171+
return nil
172+
}
173+
174+
// lowerCoroutines transforms the IR into one where all blocking functions are
175+
// turned into goroutines and blocking calls into await calls. It also makes
176+
// sure that the first coroutine is started and the coroutine scheduler will be
177+
// run.
178+
func (c *Compiler) lowerCoroutines() error {
113179
needsScheduler, err := c.markAsyncFunctions()
114180
if err != nil {
115181
return err
@@ -144,12 +210,6 @@ func (c *Compiler) LowerGoroutines() error {
144210
// main.main was set to external linkage during IR construction. Set it to
145211
// internal linkage to enable interprocedural optimizations.
146212
realMain.SetLinkage(llvm.InternalLinkage)
147-
c.mod.NamedFunction("runtime.alloc").SetLinkage(llvm.InternalLinkage)
148-
c.mod.NamedFunction("runtime.free").SetLinkage(llvm.InternalLinkage)
149-
c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.InternalLinkage)
150-
c.mod.NamedFunction("runtime.setTaskPromisePtr").SetLinkage(llvm.InternalLinkage)
151-
c.mod.NamedFunction("runtime.getTaskPromisePtr").SetLinkage(llvm.InternalLinkage)
152-
c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.InternalLinkage)
153213

154214
return nil
155215
}
@@ -173,9 +233,9 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
173233
if !sleep.IsNil() {
174234
worklist = append(worklist, sleep)
175235
}
176-
deadlockStub := c.mod.NamedFunction("runtime.deadlockStub")
177-
if !deadlockStub.IsNil() {
178-
worklist = append(worklist, deadlockStub)
236+
deadlock := c.mod.NamedFunction("runtime.deadlock")
237+
if !deadlock.IsNil() {
238+
worklist = append(worklist, deadlock)
179239
}
180240
chanSend := c.mod.NamedFunction("runtime.chanSend")
181241
if !chanSend.IsNil() {
@@ -300,7 +360,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
300360

301361
// Transform all async functions into coroutines.
302362
for _, f := range asyncList {
303-
if f == sleep || f == deadlockStub || f == chanSend || f == chanRecv {
363+
if f == sleep || f == deadlock || f == chanSend || f == chanRecv {
304364
continue
305365
}
306366

@@ -317,7 +377,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
317377
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
318378
if !inst.IsACallInst().IsNil() {
319379
callee := inst.CalledValue()
320-
if _, ok := asyncFuncs[callee]; !ok || callee == sleep || callee == deadlockStub || callee == chanSend || callee == chanRecv {
380+
if _, ok := asyncFuncs[callee]; !ok || callee == sleep || callee == deadlock || callee == chanSend || callee == chanRecv {
321381
continue
322382
}
323383
asyncCalls = append(asyncCalls, inst)
@@ -365,7 +425,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
365425
retvalAlloca = c.builder.CreateAlloca(inst.Type(), "coro.retvalAlloca")
366426
c.builder.SetInsertPointBefore(inst)
367427
data := c.builder.CreateBitCast(retvalAlloca, c.i8ptrType, "")
368-
c.createRuntimeCall("setTaskPromisePtr", []llvm.Value{frame.taskHandle, data}, "")
428+
c.createRuntimeCall("setTaskStatePtr", []llvm.Value{frame.taskHandle, data}, "")
369429
}
370430

371431
// Suspend.
@@ -403,7 +463,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
403463
var parentHandle llvm.Value
404464
if f.Linkage() == llvm.ExternalLinkage {
405465
// Exported function.
406-
// Note that getTaskPromisePtr will panic if it is called with
466+
// Note that getTaskStatePtr will panic if it is called with
407467
// a nil pointer, so blocking exported functions that try to
408468
// return anything will not work.
409469
parentHandle = llvm.ConstPointerNull(c.i8ptrType)
@@ -423,7 +483,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
423483
// Return this value by writing to the pointer stored in the
424484
// parent handle. The parent coroutine has made an alloca that
425485
// we can write to to store our return value.
426-
returnValuePtr := c.createRuntimeCall("getTaskPromisePtr", []llvm.Value{parentHandle}, "coro.parentData")
486+
returnValuePtr := c.createRuntimeCall("getTaskStatePtr", []llvm.Value{parentHandle}, "coro.parentData")
427487
alloca := c.builder.CreateBitCast(returnValuePtr, llvm.PointerType(inst.Operand(0).Type(), 0), "coro.parentAlloca")
428488
c.builder.CreateStore(inst.Operand(0), alloca)
429489
default:
@@ -502,9 +562,9 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
502562
sleepCall.EraseFromParentAsInstruction()
503563
}
504564

505-
// Transform calls to runtime.deadlockStub into coroutine suspends (without
565+
// Transform calls to runtime.deadlock into coroutine suspends (without
506566
// resume).
507-
for _, deadlockCall := range getUses(deadlockStub) {
567+
for _, deadlockCall := range getUses(deadlock) {
508568
// deadlockCall must be a call instruction.
509569
frame := asyncFuncs[deadlockCall.InstructionParent().Parent()]
510570

0 commit comments

Comments
 (0)