From c93ddb630bd145b801415cd99cdd19eadc95ad17 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 26 May 2021 00:00:25 +0200 Subject: [PATCH] compiler: skip context parameter when starting regular goroutine Do not store the context parameter (which is used for closures and function pointers) in the goroutine start parameter bundle for direct functions that don't need a context parameter. This avoids storing the (undef) context parameter and thus makes the IR to start a new goroutine simpler in most cases. This reduces code size in the channel.go and goroutines.go tests. Surprisingly, all test cases (when compiled with -target=microbit) have a changed binary, I haven't investigated why but I suppose the codegen is slightly different for the runtime.run function (which starts the main goroutine). --- compiler/compiler.go | 2 +- compiler/goroutine.go | 40 ++++++++++++++++---- compiler/testdata/goroutine-cortex-m-qemu.ll | 23 +++-------- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index 4a5092d0..f25832ac 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -23,7 +23,7 @@ import ( // Version of the compiler pacakge. Must be incremented each time the compiler // package changes in a way that affects the generated LLVM module. // This version is independent of the TinyGo version number. -const Version = 9 // last change: implement reflect.New() +const Version = 10 // last change: context parameter in go wrapper func init() { llvm.InitializeAllTargets() diff --git a/compiler/goroutine.go b/compiler/goroutine.go index d7ad6018..730fee92 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -22,6 +22,7 @@ func (b *builder) createGo(instr *ssa.Go) { var prefix string var funcPtr llvm.Value + hasContext := false if callee := instr.Call.StaticCallee(); callee != nil { // Static callee is known. This makes it easier to start a new // goroutine. @@ -29,7 +30,11 @@ func (b *builder) createGo(instr *ssa.Go) { switch value := instr.Call.Value.(type) { case *ssa.Function: // Goroutine call is regular function call. No context is necessary. - context = llvm.Undef(b.i8ptrType) + if b.Scheduler == "coroutines" { + // The context parameter is assumed to be always present in the + // coroutines scheduler. + context = llvm.Undef(b.i8ptrType) + } case *ssa.MakeClosure: // A goroutine call on a func value, but the callee is trivial to find. For // example: immediately applied functions. @@ -38,7 +43,10 @@ func (b *builder) createGo(instr *ssa.Go) { default: panic("StaticCallee returned an unexpected value") } - params = append(params, context) // context parameter + if !context.IsNil() { + params = append(params, context) // context parameter + hasContext = true + } funcPtr = b.getFunction(callee) } else if builtin, ok := instr.Call.Value.(*ssa.Builtin); ok { // We cheat. None of the builtins do any long or blocking operation, so @@ -80,6 +88,7 @@ func (b *builder) createGo(instr *ssa.Go) { var context llvm.Value funcPtr, context = b.decodeFuncValue(b.getValue(instr.Call.Value), instr.Call.Value.Type().Underlying().(*types.Signature)) params = append(params, context) // context parameter + hasContext = true switch b.Scheduler { case "none", "coroutines": // There are no additional parameters needed for the goroutine start operation. @@ -99,7 +108,7 @@ func (b *builder) createGo(instr *ssa.Go) { var callee, stackSize llvm.Value switch b.Scheduler { case "none", "tasks": - callee = b.createGoroutineStartWrapper(funcPtr, prefix, instr.Pos()) + callee = b.createGoroutineStartWrapper(funcPtr, prefix, hasContext, instr.Pos()) if b.AutomaticStackSize { // The stack size is not known until after linking. Call a dummy // function that will be replaced with a load from a special ELF @@ -145,7 +154,12 @@ func (b *builder) createGo(instr *ssa.Go) { // allows a single (pointer) argument to the newly started goroutine. Also, it // ignores the return value because newly started goroutines do not have a // return value. -func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix string, pos token.Pos) llvm.Value { +// +// The hasContext parameter indicates whether the context parameter (the second +// to last parameter of the function) is used for this wrapper. If hasContext is +// false, the parameter bundle is assumed to have no context parameter and undef +// is passed instead. +func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix string, hasContext bool, pos token.Pos) llvm.Value { var wrapper llvm.Value builder := c.ctx.NewBuilder() @@ -192,8 +206,15 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri // Create the list of params for the call. paramTypes := fn.Type().ElementType().ParamTypes() - params := llvmutil.EmitPointerUnpack(builder, c.mod, wrapper.Param(0), paramTypes[:len(paramTypes)-1]) - params = append(params, llvm.Undef(c.i8ptrType)) + paramTypes = paramTypes[:len(paramTypes)-1] // strip parentHandle parameter + if !hasContext { + paramTypes = paramTypes[:len(paramTypes)-1] // strip context parameter + } + params := llvmutil.EmitPointerUnpack(builder, c.mod, wrapper.Param(0), paramTypes) + if !hasContext { + params = append(params, llvm.Undef(c.i8ptrType)) // add dummy context parameter + } + params = append(params, llvm.Undef(c.i8ptrType)) // add dummy parentHandle parameter // Create the call. builder.CreateCall(fn, params, "") @@ -255,8 +276,11 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri // Get the function pointer. fnPtr := params[len(params)-1] - // Ignore the last param, which isn't used anymore. - // TODO: avoid this extra "parent handle" parameter in most functions. + // The last parameter in the packed object has somewhat of a dual role. + // Inside the parameter bundle it's the function pointer, stored right + // after the context pointer. But in the IR call instruction, it's the + // parentHandle function that's always undef outside of the coroutines + // scheduler. Thus, make the parameter undef here. params[len(params)-1] = llvm.Undef(c.i8ptrType) // Create the call. diff --git a/compiler/testdata/goroutine-cortex-m-qemu.ll b/compiler/testdata/goroutine-cortex-m-qemu.ll index 3a263fec..a00d49be 100644 --- a/compiler/testdata/goroutine-cortex-m-qemu.ll +++ b/compiler/testdata/goroutine-cortex-m-qemu.ll @@ -9,9 +9,6 @@ target triple = "armv7m-none-eabi" %"internal/task.state" = type { i32, i32* } %runtime.chanSelectState = type { %runtime.channel*, i8* } -@"main.regularFunctionGoroutine$pack" = private unnamed_addr constant { i32, i8* } { i32 5, i8* undef } -@"main.inlineFunctionGoroutine$pack" = private unnamed_addr constant { i32, i8* } { i32 5, i8* undef } - declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*) define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr { @@ -22,7 +19,7 @@ entry: define hidden void @main.regularFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { entry: %stacksize = call i32 @"internal/task.getGoroutineStackSize"(i32 ptrtoint (void (i8*)* @"main.regularFunction$gowrapper" to i32), i8* undef, i8* undef) - call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.regularFunction$gowrapper" to i32), i8* bitcast ({ i32, i8* }* @"main.regularFunctionGoroutine$pack" to i8*), i32 %stacksize, i8* undef, i8* null) + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.regularFunction$gowrapper" to i32), i8* nonnull inttoptr (i32 5 to i8*), i32 %stacksize, i8* undef, i8* null) ret void } @@ -30,12 +27,8 @@ declare void @main.regularFunction(i32, i8*, i8*) define linkonce_odr void @"main.regularFunction$gowrapper"(i8* %0) unnamed_addr #0 { entry: - %1 = bitcast i8* %0 to i32* - %2 = load i32, i32* %1, align 4 - %3 = getelementptr inbounds i8, i8* %0, i32 4 - %4 = bitcast i8* %3 to i8** - %5 = load i8*, i8** %4, align 4 - call void @main.regularFunction(i32 %2, i8* %5, i8* undef) + %unpack.int = ptrtoint i8* %0 to i32 + call void @main.regularFunction(i32 %unpack.int, i8* undef, i8* undef) ret void } @@ -46,7 +39,7 @@ declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*) define hidden void @main.inlineFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { entry: %stacksize = call i32 @"internal/task.getGoroutineStackSize"(i32 ptrtoint (void (i8*)* @"main.inlineFunctionGoroutine$1$gowrapper" to i32), i8* undef, i8* undef) - call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.inlineFunctionGoroutine$1$gowrapper" to i32), i8* bitcast ({ i32, i8* }* @"main.inlineFunctionGoroutine$pack" to i8*), i32 %stacksize, i8* undef, i8* null) + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.inlineFunctionGoroutine$1$gowrapper" to i32), i8* nonnull inttoptr (i32 5 to i8*), i32 %stacksize, i8* undef, i8* null) ret void } @@ -57,12 +50,8 @@ entry: define linkonce_odr void @"main.inlineFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #1 { entry: - %1 = bitcast i8* %0 to i32* - %2 = load i32, i32* %1, align 4 - %3 = getelementptr inbounds i8, i8* %0, i32 4 - %4 = bitcast i8* %3 to i8** - %5 = load i8*, i8** %4, align 4 - call void @"main.inlineFunctionGoroutine$1"(i32 %2, i8* %5, i8* undef) + %unpack.int = ptrtoint i8* %0 to i32 + call void @"main.inlineFunctionGoroutine$1"(i32 %unpack.int, i8* undef, i8* undef) ret void }